From 240d1a3bbf9795ab6cff478e5bcbdc31eb7bf2f6 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Wed, 19 Jun 2024 08:40:10 +0200 Subject: [PATCH 001/393] LADX: Adding 'Option Groups' to the player options page. (#3560) * Adding 'Option Groups' to the LADX player options page. * Moved 'Miscellaneous' group to the logic effecting groups. --- worlds/ladx/Options.py | 40 +++++++++++++++++++++++++++++++++++++++- worlds/ladx/__init__.py | 4 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 758b5a6a1ebb..be90ee597469 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -3,7 +3,7 @@ import os.path import typing import logging -from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions +from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup from collections import defaultdict import Utils @@ -493,6 +493,44 @@ class AdditionalWarpPoints(DefaultOffToggle): [Off] No change """ +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): diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 97daf7e26bdb..21876ed671e2 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -24,7 +24,7 @@ from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions +from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups from .Rom import LADXDeltaPatch, get_base_rom_path DEVELOPER_MODE = False @@ -65,7 +65,7 @@ class LinksAwakeningWebWorld(WebWorld): ["zig"] )] theme = "dirt" - + option_groups = ladx_option_groups class LinksAwakeningWorld(World): """ From 9bb3947d7e7f9a6575cf22f3c718762de38b71e6 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Wed, 19 Jun 2024 03:59:10 -0700 Subject: [PATCH 002/393] Doom 2, Heretic: fix missing items (Doom2 Megasphere, Heretic Torch) (#3561) for doom 2, some of the armor and health weights were nudged down to compensate for the addition of the megasphere for heretic, the torch was just added without changing anything else, as I felt doing so would negatively impact the distribution of artifacts (and personally I already feel there's too few in a game) --- worlds/doom_ii/__init__.py | 12 +++++++----- worlds/heretic/__init__.py | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) 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/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) From 903a0bab1a61773eb84cd6f41fb2cd81fe178cdc Mon Sep 17 00:00:00 2001 From: eudaimonistic <94811100+eudaimonistic@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:12:25 -0400 Subject: [PATCH 003/393] Docs: Change setup_en.md to use Latest releases page (#3543) * Change setup_en.md to use Latest releases page Really simple change to point users to the Latest release page instead of the Releases page. Saw a user accidentally download 0.3.6 because it was the last item on the page (they're accustomed to scrolling down to the bottom of the page in GitHub for the Assets section), and this change prevents that outright. * Update setup_en.md Rewrite text and link to restore semantic compatibility. --- worlds/generic/docs/setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index ef2413378960..6e65459851e3 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 Lastest 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. From f515a085dbab0b058fa0d310fb542ff13c3a1089 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 19 Jun 2024 09:20:47 -0500 Subject: [PATCH 004/393] The Messenger: Fix missing rules for Double Swing Saws (#3562) * The Messenger: Fix missing rules for Double Swing Saws * i put it in the wrong dictionary * remove unnecessary call --- worlds/messenger/rules.py | 4 ++++ 1 file changed, 4 insertions(+) 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": From 4f514e5944b0ffb9ba47f5aed516696af40daceb Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Thu, 20 Jun 2024 21:54:38 +1000 Subject: [PATCH 005/393] Muse Dash: Song name change (#3572) * Change the song name of the removed song to the one replacing it. * Make it not part of Streamable songs for now. --- worlds/musedash/MuseDashData.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index d822a3dc3839..49c26e45799d 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -553,7 +553,7 @@ 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 From ce37bed7c60d70ea3366f6887df9435fc6753e47 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 21 Jun 2024 14:54:19 +0200 Subject: [PATCH 006/393] WebHost: fix accidental robots.txt capture (#3502) --- WebHostLib/robots.py | 3 ++- WebHostLib/static/{robots.txt => robots_file.txt} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename WebHostLib/static/{robots.txt => robots_file.txt} (100%) 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 From 40c9dfd3bfd8e3ec7a78dc605ebaaa5f2d94cabf Mon Sep 17 00:00:00 2001 From: Mewlif <68133186+jonloveslegos@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:21:46 -0400 Subject: [PATCH 007/393] Undertale: Fixes a major logic bug, and updates Undertale to use the new Options API (#3528) * Updated the options definitions to the new api * Fixed the wrong base class being used for UndertaleOptions * Undertale: Added get_filler_item_name to Undertale, changed multiworld.per_slot_randoms to self.random, removed some unused imports in options.py, and fixed rules.py still using state.multiworld instead of world.options, and simplified the set_completion_rules function in rules.py * Undertale: Fixed it trying to add strings to the finished item pool * fixed 1000g item not being in the key items pool for Undertale * Removed ".copy()" for the junk_weights, reformatted the requested lines to have less new lines, and changed "itempool += [self.create_filler()]" to "itempool.append(self.create_filler())" --- worlds/undertale/Items.py | 2 +- worlds/undertale/Options.py | 32 ++++---- worlds/undertale/Rules.py | 67 ++++++++------- worlds/undertale/__init__.py | 153 ++++++++++++++++++----------------- 4 files changed, 136 insertions(+), 118 deletions(-) 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) From d00abe7b8e4f83cac22060dd848c26be159e319e Mon Sep 17 00:00:00 2001 From: StripesOO7 <54711792+StripesOO7@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:50:20 +0200 Subject: [PATCH 008/393] OOT: Adds Options to slot_data for poptracker-pack (#3570) * Add imo all needed options to fill_slot_data that are worth tracking in the poptracker pack. This is aimed at providing information for the oot poptracker-pack for autofilling of settings within this pack. * cap line length at 120 and reorganize list --------- Co-authored-by: StripesOO7 <54711792+StripeesOO7@users.noreply.github.com> --- worlds/oot/__init__.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 34b3935fec4e..270062f4bc64 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1173,10 +1173,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): From 60a26920e1485a770e07b65b2321187d92c577bc Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Sat, 22 Jun 2024 19:32:10 +0200 Subject: [PATCH 009/393] LADX: Probably fix generation error that palex had --- worlds/ladx/LADXR/locations/droppedKey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}: From 5ca31533dca0176a213513e999c4a971659b928a Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:00:15 -0500 Subject: [PATCH 010/393] Tests: give seed on default tests and fix execnet error (#3520) * output seed of default tests * test execnet fix * try failing with interpolated string * Update bases.py * try without tryexcept * Update bases.py * Update bases.py * remove fake exception * fix indent * actually fix the execnet issue --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- test/bases.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bases.py b/test/bases.py index 928ab5b1e585..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, From 1ab1aeff15493034233d21f9e1fe9c68b8a50433 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 23 Jun 2024 07:50:00 +0200 Subject: [PATCH 011/393] Core: update required_server_version to 0.5.0 (#3580) --- worlds/AutoWorld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index b5f0fd172053..af067e5cb8a6 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -280,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() From 935c94dc809293d6c7ff7bdb3b33703d12f56c10 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Tue, 25 Jun 2024 11:15:12 -0700 Subject: [PATCH 012/393] Installer: Fix .apworld registration (#3588) --- inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index f2e850e07f20..f097500f7d7d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -219,7 +219,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: ""; From 07dd8f0671093d8dce10e698651d8eab6f1f7e8f Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 25 Jun 2024 14:15:51 -0400 Subject: [PATCH 013/393] LTTP: Add Missing Blind's Cell rule (#3589) --- worlds/alttp/Rules.py | 6 ++---- worlds/alttp/test/dungeons/TestThievesTown.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index a9c8d5456a42..171c82f9b226 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -405,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)) 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)']], From 6c54b3596b74c9b9ce0cfd5f27a3a697866dc544 Mon Sep 17 00:00:00 2001 From: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Wed, 26 Jun 2024 06:19:16 -0500 Subject: [PATCH 014/393] Yoshi's Island: Fix client giving victory randomly (#3586) * Create d * Create d * Delete worlds/mariomissing/d * Delete mariomissing directory * Create d * Add files via upload * Delete worlds/mariomissing/d * Delete worlds/mariomissing directory * Add files via upload * Delete worlds/sai2 directory * fix dumb client bug --- worlds/yoshisisland/Client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 98b0ff1a8c90..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 From 5882ce7380cf857ef5be3bf293521000d9583686 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:51:27 +0200 Subject: [PATCH 015/393] Various worlds: Fix more absolute world imports (#3510) * Adventure: remove absolute imports * Alttp: remove absolute imports (all but tests) * Aquaria: remove absolute imports in tests running tests from apworld may fail (on 3.8 and maybe in the future) otherwise * DKC3: remove absolute imports * LADX: remove absolute imports * Overcooked 2: remove absolute imports in tests running tests from apworld may fail otherwise * Rogue Legacy: remove absolute imports in tests running tests from apworld may fail otherwise * SC2: remove absolute imports * SMW: remove absolute imports * Subnautica: remove absolute imports in tests running tests from apworld may fail otherwise * Zillion: remove absolute imports in tests running tests from apworld may fail otherwise --- worlds/adventure/Rules.py | 4 +--- worlds/alttp/Client.py | 2 +- worlds/alttp/EntranceShuffle.py | 2 +- worlds/alttp/Options.py | 2 +- worlds/alttp/Regions.py | 4 ++-- worlds/aquaria/test/test_beast_form_access.py | 2 +- worlds/aquaria/test/test_bind_song_access.py | 2 +- .../test/test_bind_song_option_access.py | 4 ++-- .../aquaria/test/test_confined_home_water.py | 2 +- worlds/aquaria/test/test_dual_song_access.py | 2 +- worlds/aquaria/test/test_energy_form_access.py | 2 +- worlds/aquaria/test/test_fish_form_access.py | 2 +- worlds/aquaria/test/test_li_song_access.py | 2 +- worlds/aquaria/test/test_light_access.py | 2 +- worlds/aquaria/test/test_nature_form_access.py | 2 +- ...est_no_progression_hard_hidden_locations.py | 2 +- .../test_progression_hard_hidden_locations.py | 3 +-- worlds/aquaria/test/test_spirit_form_access.py | 2 +- worlds/aquaria/test/test_sun_form_access.py | 2 +- .../test/test_unconfine_home_water_via_both.py | 2 +- ...est_unconfine_home_water_via_energy_door.py | 2 +- ...est_unconfine_home_water_via_transturtle.py | 2 +- worlds/dkc3/Client.py | 2 +- worlds/ladx/Tracker.py | 2 +- worlds/overcooked2/test/TestOvercooked2.py | 9 +++++---- worlds/rogue_legacy/test/TestUnique.py | 4 ++-- worlds/sc2/Client.py | 18 +++++++++--------- worlds/sc2/ClientGui.py | 12 ++++++------ worlds/smw/Client.py | 6 +++--- worlds/subnautica/test/__init__.py | 2 +- worlds/zillion/test/TestOptions.py | 2 +- worlds/zillion/test/TestReproducibleRandom.py | 2 +- worlds/zillion/test/__init__.py | 2 +- 33 files changed, 55 insertions(+), 57 deletions(-) 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/alttp/Client.py b/worlds/alttp/Client.py index 8b7444655f84..a0b28829f4bb 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -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/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/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..bf0ace478e2e 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): 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..85fc2bd45a66 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): 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..71432539d0da 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): 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..40cb5336484e 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 diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 390fc40b295d..026a175206fc 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): diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index a6eec0da5dd3..40a087a7fb5a 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): 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/dkc3/Client.py b/worlds/dkc3/Client.py index 25b058f05f1b..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: 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/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/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 8b9269cb0a03..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 diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 664bd5cee741..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): diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 91d59ab3b5f3..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] 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/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): From 77304a87433f4b1a8552dd6c1a81fbc8a54f7df6 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 27 Jun 2024 07:00:20 -0400 Subject: [PATCH 016/393] TUNIC: Update game info page with more tips (#3591) * More minor updates to game info page * Fix grammar --- worlds/tunic/docs/en_TUNIC.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 0bb6fa52e0fa..27df4ce38be4 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -82,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. From b8f78af5065c7e62bc3ed8e6fa71637d5a81f2c6 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 27 Jun 2024 08:01:35 -0400 Subject: [PATCH 017/393] TUNIC: Fix minor logic bug in upper Zig (#3576) * Add note about bushes to logic section of readme * Fix missing logic on bridge switch chest in upper zig * Revise upper zig rule change to account for ER --- worlds/tunic/er_rules.py | 4 +++- worlds/tunic/rules.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index bbee212f5d5a..2706d9595d00 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1464,8 +1464,10 @@ 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)) diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index e0a2c305101b..e6bc490192ae 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -304,6 +304,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # 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)) From e07a2667ae590903c9b6e191c3656f0e0f8283ba Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Thu, 27 Jun 2024 14:02:03 +0200 Subject: [PATCH 018/393] SC2 Tracker: Migrate icons away from sc2legacy (#3595) --- WebHostLib/tracker.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) 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", From d4c00ed26743b56823c4bd191502b6c16bf9e58a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 29 Jun 2024 03:00:32 +0200 Subject: [PATCH 019/393] CommonClient: fix /received with items from Server (#3597) --- CommonClient.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 19dd44f592a4..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 @@ -862,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}") From 1c817e1eb7ae2d9b2a592cfc6e5b1a9f407aabb8 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 30 Jun 2024 09:13:00 +1000 Subject: [PATCH 020/393] Muse Dash: Update installation guides to recommend installing v0.6.1. (#3594) * Update installation guides to recommend installing v0.6.1. * Fix spanish spacing. * Apply spanish changes. --- worlds/musedash/docs/setup_en.md | 7 ++++--- worlds/musedash/docs/setup_es.md | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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. From 6191ff4b47eaccbcce3d1ed44d5cc340d6b33bfa Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Sun, 30 Jun 2024 01:14:39 +0200 Subject: [PATCH 021/393] LADX: Fixed Display Names In Options Page (#3584) * Fixed option group display names. * Fixed display names for -at the moment- unused options. --- worlds/ladx/Options.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index be90ee597469..c5dcc080537c 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -52,6 +52,7 @@ 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): @@ -68,7 +69,8 @@ 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 @@ -113,6 +115,7 @@ class APTitleScreen(DefaultOnToggle): class BossShuffle(Choice): + display_name = "Boss Shuffle" none = 0 shuffle = 1 random = 2 @@ -120,6 +123,7 @@ class BossShuffle(Choice): class DungeonItemShuffle(Choice): + display_name = "Dungeon Item Shuffle" option_original_dungeon = 0 option_own_dungeons = 1 option_own_world = 2 @@ -291,6 +295,7 @@ 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 @@ -466,6 +471,7 @@ class Music(Choice, LADXROption): [Shuffled] Shuffled Music [Off] No music """ + display_name = "Music" ladxr_name = "music" option_vanilla = 0 option_shuffled = 1 @@ -485,6 +491,7 @@ 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): @@ -492,6 +499,7 @@ 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 """ + display_name = "Additional Warp Points" ladx_option_groups = [ OptionGroup("Goal Options", [ From 2424fb0c5bfc4962cdec028431a8bddf7f5777fa Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 30 Jun 2024 09:15:13 +1000 Subject: [PATCH 022/393] Muse Dash: 6th Anniversary Song update (#3593) * 6th Anniversary Update songs. * Forgot to fix the name of Heartbeat. --- worlds/musedash/MuseDashData.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 49c26e45799d..6f48d6af9fdd 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -557,3 +557,13 @@ 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| From 55cb81d48721c9270a9e9790e109e53dfd858ccb Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:17:00 -0400 Subject: [PATCH 023/393] TUNIC: Update victory condition (#3579) * Add hero relics to victory condition * Update __init__.py * Remove unneeded local variables for options * Use has_group_unique * fix spacing --- worlds/tunic/__init__.py | 23 ++++++++++++++++------- worlds/tunic/er_rules.py | 3 ++- worlds/tunic/items.py | 13 ++++++------- worlds/tunic/rules.py | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 624208da3a0b..92834d96b07f 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 @@ -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) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 2706d9595d00..d9348628ce9c 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -991,7 +991,8 @@ 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)))) # 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 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/rules.py b/worlds/tunic/rules.py index e6bc490192ae..97270b5a2a81 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -128,7 +128,7 @@ def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> No or has_ice_grapple_logic(False, state, player, options, ability_unlocks) 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 \ + else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) and state.has_group_unique("Hero Relics", player, 6)) and \ has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) and \ state.has_any({lantern, laurels}, player) From 192f1b3fae415e820940f0c4f45d97d9c7d24252 Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Sat, 29 Jun 2024 16:18:09 -0700 Subject: [PATCH 024/393] Update Raft option text, setup guide text (#3272) * Update Raft option text, setup guide * Address comments * Address PR comments --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/raft/Options.py | 14 ++++++++++---- worlds/raft/docs/setup_en.md | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index b9d0a2298a07..696d4dbab477 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -32,7 +32,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 @@ -53,7 +59,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,8 +73,7 @@ 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 = { 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. From 31bd5e3ebcc2b135fa25672b2d3cd0658a3c0f25 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 29 Jun 2024 19:19:36 -0400 Subject: [PATCH 025/393] OOT: Add keys item_name_group (#3218) * Add keys item_name_group * Pep8ify * Capitalizing Keys cause Bottles is capitalized, also putting it in the clearly marked hint groups area --- worlds/oot/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 270062f4bc64..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() From 52a13d38e9d3295d26e41895a922f6f91f4ae85f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:47:40 -0500 Subject: [PATCH 026/393] Tests: fix error reporting in test_default_all_state_can_reach_everything (#3601) --- test/general/test_reachability.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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"): From e95bb5ea56caae2ba8812c54f8d316eb1cc9a4e9 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 1 Jul 2024 21:47:49 +0200 Subject: [PATCH 027/393] WebHost: Better host room (#3496) * add Range= to log, making responses a lot smaller for massive rooms * switch xhr to fetch * post the form using fetch if possible * also refresh log faster while waiting for command echo / response * do not follow redirect, saving a request * do not post empty body * smooth-scroll the log view * paste the log into the div when loading the HTML (up to 1MB, rest will be `fetch`ed) * fix duplicate charset in display_log response --- WebHostLib/misc.py | 51 +++++++++++----- WebHostLib/templates/hostRoom.html | 93 +++++++++++++++++++++++++----- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 5072f113bd54..4b7cb475fc8b 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 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,36 @@ 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(path: str, offset: int = 0) -> Iterator[bytes]: + with open(path, "rb") as log: + 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 @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: + 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(file_path, int(start)), mimetype="text/plain", status=206) + return Response(_read_log(file_path), 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 +150,21 @@ 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: + raw_size = 0 + fragments: List[str] = [] + for block in _read_log(os.path.join("logs", str(room.id) + ".txt")): + 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/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 %}
From 401606e8e3014a2222d573ca54fc28cff7a87c65 Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:34:06 -0400 Subject: [PATCH 028/393] Docs: Clarify docs for `create_items` stage (#3600) * Clarify docs re: `create_items` stage * adjust wording after feedback * adjust wording after more feedback --- docs/world api.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 756ef3f31fdd..6551f2260416 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -456,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)` From b6925c593e202cca7f627eb3592e06c149854858 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:03:55 +0200 Subject: [PATCH 029/393] WebHost: Log: handle FileNotFoundError (#3603) --- WebHostLib/misc.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 4b7cb475fc8b..01c1ad84a707 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -1,6 +1,6 @@ import datetime import os -from typing import Dict, Iterator, List, Tuple, 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,13 +97,13 @@ def new_room(seed: UUID): return redirect(url_for("host_room", room=room.id)) -def _read_log(path: str, offset: int = 0) -> Iterator[bytes]: - with open(path, "rb") as log: - 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 +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/') @@ -114,6 +114,7 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: if room.owner == session["_id"]: file_path = os.path.join("logs", str(room.id) + ".txt") try: + log = open(file_path, "rb") range_header = request.headers.get("Range") if range_header: range_type, range_values = range_header.split('=') @@ -121,8 +122,8 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: 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(file_path, int(start)), mimetype="text/plain", status=206) - return Response(_read_log(file_path), mimetype="text/plain") + 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.", @@ -152,15 +153,16 @@ def host_room(room: UUID): def get_log(max_size: int = 1024000) -> str: try: - raw_size = 0 - fragments: List[str] = [] - for block in _read_log(os.path.join("logs", str(room.id) + ".txt")): - if raw_size + len(block) > max_size: - fragments.append("…") - break - raw_size += len(block) - fragments.append(block.decode("utf-8")) - return "".join(fragments) + 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 "" From 93617fa54609108fe115cf549a124ff444961548 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:59:26 +0200 Subject: [PATCH 030/393] The Witness: mypy compliance (#3112) * Make witness apworld mostly pass mypy * Fix all remaining mypy errors except the core ones * I'm a goofy stupid poopoo head * Two more fixes * ruff after merge * Mypy for new stuff * Oops * Stricter ruff rules (that I already comply with :3) * Deprecated ruff thing * wait no i lied * lol super nevermind * I can actually be slightly more specific * lint --- worlds/witness/__init__.py | 28 +-- worlds/witness/data/static_items.py | 12 +- worlds/witness/data/static_locations.py | 14 +- worlds/witness/data/static_logic.py | 68 +++---- worlds/witness/data/utils.py | 30 +-- worlds/witness/hints.py | 73 ++++--- worlds/witness/locations.py | 6 +- worlds/witness/player_items.py | 25 ++- worlds/witness/player_logic.py | 246 ++++++++++++------------ worlds/witness/regions.py | 40 ++-- worlds/witness/ruff.toml | 6 +- worlds/witness/rules.py | 20 +- 12 files changed, 299 insertions(+), 269 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ecab25db3d71..455c87d8e0d1 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() @@ -279,7 +281,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 +303,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 +380,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 +401,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/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 05b3cf3a98e4..b62c59b00ae1 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 - - else: - return frozenset(all_options) + or entity_hex == "0x1C349" + ) - 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"}: @@ -525,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. @@ -640,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: @@ -664,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. @@ -679,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 @@ -695,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. """ @@ -808,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: """ @@ -828,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: str - - 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) From 95110c478733a315cb61ccd919fd3ca5f4801dfa Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:34:17 +0200 Subject: [PATCH 031/393] The Witness: Fix door shuffle being completely broken --- worlds/witness/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index b62c59b00ae1..e8d11f43f51c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -197,7 +197,7 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # 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" + 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. From 50f7a79ea75e2b7eb0ff4c3881408485b4e9ec4e Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 2 Jul 2024 19:32:01 -0700 Subject: [PATCH 032/393] Zillion: new map generation feature (#3604) --- worlds/zillion/__init__.py | 6 +++--- worlds/zillion/options.py | 34 +++++++++++++++++++++++++-------- worlds/zillion/requirements.txt | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) 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/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..b4f554902f48 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@4a2fec0aa1c529df866e510cdfcf6dca4d53679b#0.8.0 typing-extensions>=4.7, <5 From f6735745b619a83d1631a37cf1be7c8208cc5a83 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 3 Jul 2024 06:39:08 -0700 Subject: [PATCH 033/393] Core: Fix !remaining (#3611) --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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.") From 315e0c89e2f0f851e72bec651da10f917b485713 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 3 Jul 2024 12:13:16 -0400 Subject: [PATCH 034/393] Docs: Lastest -> Latest (#3616) --- worlds/generic/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index 6e65459851e3..22622cd0e94d 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -12,7 +12,7 @@ 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 GitHub: -[Archipelago Lastest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). +[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. From d4d0a3e945274b6470c86dbd853922578679e97c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 5 Jul 2024 16:36:55 -0400 Subject: [PATCH 035/393] TUNIC: Make the shop checks require a sword --- worlds/tunic/er_rules.py | 10 ++++++++++ worlds/tunic/rules.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index d9348628ce9c..2652a5b148de 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1538,3 +1538,13 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: has_ability(state, player, prayer, options, ability_unlocks)) set_rule(multiworld.get_location("Library Fuse", player), lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + + # 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/rules.py b/worlds/tunic/rules.py index 97270b5a2a81..b9dbc1e226b1 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -337,3 +337,13 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) 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)) + + # 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)) From ca766288137e3f7973f71a13517cd32d3e08fc8e Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:37:32 -0700 Subject: [PATCH 036/393] sc2: Fixing typo in itemgroups.py causing spurious item groups with 2 letters chopped off (#3612) --- worlds/sc2/ItemGroups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4054a9f15fb86af4de5f4a2fe2241098b0193290 Mon Sep 17 00:00:00 2001 From: Louis M Date: Fri, 5 Jul 2024 16:40:26 -0400 Subject: [PATCH 037/393] Aquaria: Renaming some locations for consistency (#3533) * Change 'The Body main area' by 'The Body center area' for consistency * Renaming some locations for consistency * Adding a line for standard * Replacing Cathedral by Mithalas Cathedral and addin Blind goal option * Client option renaming for consistency * Fix death link not working * Removing death link from the option to put it client side * Changing Left to Right --- worlds/aquaria/Locations.py | 13 +++++++------ worlds/aquaria/Options.py | 12 ++++++++++-- worlds/aquaria/Regions.py | 18 +++++++++--------- worlds/aquaria/__init__.py | 3 ++- worlds/aquaria/test/__init__.py | 10 +++++----- worlds/aquaria/test/test_energy_form_access.py | 4 ++-- worlds/aquaria/test/test_li_song_access.py | 2 +- worlds/aquaria/test/test_nature_form_access.py | 2 +- ...est_no_progression_hard_hidden_locations.py | 4 ++-- .../test_progression_hard_hidden_locations.py | 4 ++-- worlds/aquaria/test/test_spirit_form_access.py | 2 +- 11 files changed, 42 insertions(+), 32 deletions(-) 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_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index bf0ace478e2e..82d8e89a0066 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -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_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index 85fc2bd45a66..f615fb10c640 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -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_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 71432539d0da..1d3b8f4150eb 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -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 40cb5336484e..f015b26de10b 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -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 026a175206fc..a1493c5d0f39 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -15,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", @@ -34,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 40a087a7fb5a..3bcbd7d72e02 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -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", From e7a8e195e67b8ef3d220e0c93083157786bd2e5c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 5 Jul 2024 16:50:12 -0400 Subject: [PATCH 038/393] TUNIC: Use fewer parameters in helper functions (#3356) * Clean these functions up, get the hell out of here 5 parameter function * Clean up a bunch of rules that no longer need to be multi-lined since the functions are shorter * Clean up some range functions * Update to use world instead of player like Vi recommended * Fix merge conflict * Fix after merge --- worlds/tunic/__init__.py | 12 +- worlds/tunic/er_rules.py | 447 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 2 +- worlds/tunic/rules.py | 188 ++++++++-------- 4 files changed, 314 insertions(+), 335 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 92834d96b07f..f63193e6aeef 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -200,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: @@ -258,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) @@ -309,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) @@ -387,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/er_rules.py b/worlds/tunic/er_rules.py index 2652a5b148de..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"]) @@ -992,7 +985,8 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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) - and state.has_group_unique("Hero Relics", player, 6)))) + 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 @@ -1228,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: @@ -1253,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: @@ -1268,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: @@ -1304,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( @@ -1317,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), @@ -1380,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), @@ -1420,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), @@ -1429,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), @@ -1470,7 +1459,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) 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), @@ -1478,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)) @@ -1516,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), @@ -1529,15 +1518,15 @@ 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), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 9d25137ba469..8689a51b7601 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] diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index b9dbc1e226b1..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 state.has_group_unique("Hero Relics", player, 6)) 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,19 +280,17 @@ 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), @@ -313,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), @@ -326,17 +316,17 @@ 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), From bfac100567d56a088a0538c61c10c61fbab06100 Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Fri, 5 Jul 2024 16:54:35 -0400 Subject: [PATCH 039/393] MLSS: Fix for missing cutscene trigger --- worlds/mlss/data/basepatch.bsdiff | Bin 17615 -> 17596 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 156d28f346e8fa38fb2d1c4a3ce1b9e01342e848..8f9324995ec4c9ef9992397d64b33847c207600d 100644 GIT binary patch literal 17596 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 literal 17615 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 D+|RcY From f99ee77325c2c19a5658459e09a90a4ce48ed69b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 6 Jul 2024 13:40:55 +0200 Subject: [PATCH 040/393] The Witness: Add some unit tests (#3328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add hidden early symbol item option, make some unit tests * Add early symbol item false to the arrows test * I guess it's not an issue * more tests * assertEqual * cleanup * add minimum symbols test for all 3 modes * Formatting * Add more minimal beatability tests * one more for the road * I HATE THIS AAAAAAAAAAAHHHHHHHHHHH WHY DID WE GO WITH OPTIONS * loiaqeäsdhgalikSDGHjasDÖKHGASKLDÖGHJASKLJGHJSAÖkfaöslifjasöfASGJÖASDLFGJ'sklgösLGIKsdhJLGÖsdfjälghklDASFJghjladshfgjasdfälkjghasdöLfghasd-kjgjASDLÖGHAESKDLJGJÖsdaLGJHsadöKGjFDSLAkgjölSÄDghbASDFKGjasdLJGhjLÖSDGHLJASKDkgjldafjghjÖLADSFghäasdökgjäsadjlgkjsadkLHGsaDÖLGSADGÖLwSdlgkJLwDSFÄLHBJsaöfdkHweaFGIoeWjvlkdösmVJÄlsafdJKhvjdsJHFGLsdaövhWDsköLV-ksdFJHGVöSEKD * fix imports (within apworld needs to be relative) * Update worlds/witness/options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Sure * good suggestion * subtest * Add some EP shuffle unit tests, also an explicit event-checking unit test * add more tests yay * oops * mypy * Update worlds/witness/options.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Collapse into one test :( * More efficiency * line length * More collapsing * Cleanup and docstrings --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/witness/__init__.py | 31 +-- worlds/witness/options.py | 11 +- worlds/witness/test/__init__.py | 161 +++++++++++++++ worlds/witness/test/test_auto_elevators.py | 66 +++++++ .../test/test_disable_non_randomized.py | 37 ++++ worlds/witness/test/test_door_shuffle.py | 24 +++ worlds/witness/test/test_ep_shuffle.py | 54 +++++ worlds/witness/test/test_lasers.py | 185 ++++++++++++++++++ .../witness/test/test_roll_other_options.py | 58 ++++++ worlds/witness/test/test_symbol_shuffle.py | 74 +++++++ 10 files changed, 685 insertions(+), 16 deletions(-) create mode 100644 worlds/witness/test/__init__.py create mode 100644 worlds/witness/test/test_auto_elevators.py create mode 100644 worlds/witness/test/test_disable_non_randomized.py create mode 100644 worlds/witness/test/test_door_shuffle.py create mode 100644 worlds/witness/test/test_ep_shuffle.py create mode 100644 worlds/witness/test/test_lasers.py create mode 100644 worlds/witness/test/test_roll_other_options.py create mode 100644 worlds/witness/test/test_symbol_shuffle.py diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 455c87d8e0d1..254064098db9 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -185,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. 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/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) From 9b22458f44083e7dd54f261f6fd24db720d6de29 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 7 Jul 2024 16:04:25 +0300 Subject: [PATCH 041/393] Stardew Valley 6.x.x: The Content Update (#3478) Focus of the Update: Compatibility with Stardew Valley 1.6 Released on March 19th 2024 This includes randomization for pretty much all of the new content, including but not limited to - Raccoon Bundles - Booksanity - Skill Masteries - New Recipes, Craftables, Fish, Maps, Farm Type, Festivals and Quests This also includes a significant reorganisation of the code into "Content Packs", to allow for easier modularity of various game mechanics between the settings and the supported mods. This improves maintainability quite a bit. In addition to that, a few **very** requested new features have been introduced, although they weren't the focus of this update - Walnutsanity - Player Buffs - More customizability in settings, such as shorter special orders, ER without farmhouse - New Remixed Bundles --- worlds/stardew_valley/__init__.py | 99 ++- worlds/stardew_valley/bundles/bundle.py | 98 ++- worlds/stardew_valley/bundles/bundle_item.py | 38 +- worlds/stardew_valley/bundles/bundle_room.py | 25 +- worlds/stardew_valley/bundles/bundles.py | 144 ++-- worlds/stardew_valley/content/__init__.py | 107 +++ .../stardew_valley/content/content_packs.py | 31 + .../content/feature/__init__.py | 4 + .../content/feature/booksanity.py | 72 ++ .../content/feature/cropsanity.py | 42 + .../content/feature/fishsanity.py | 101 +++ .../content/feature/friendsanity.py | 139 +++ worlds/stardew_valley/content/game_content.py | 117 +++ worlds/stardew_valley/content/mod_registry.py | 7 + .../stardew_valley/content/mods/__init__.py | 0 .../stardew_valley/content/mods/archeology.py | 20 + .../content/mods/big_backpack.py | 7 + .../content/mods/boarding_house.py | 13 + .../stardew_valley/content/mods/deepwoods.py | 28 + .../content/mods/distant_lands.py | 17 + worlds/stardew_valley/content/mods/jasper.py | 14 + worlds/stardew_valley/content/mods/magic.py | 10 + .../stardew_valley/content/mods/npc_mods.py | 88 ++ .../stardew_valley/content/mods/skill_mods.py | 25 + .../content/mods/skull_cavern_elevator.py | 7 + worlds/stardew_valley/content/mods/sve.py | 126 +++ worlds/stardew_valley/content/mods/tractor.py | 7 + worlds/stardew_valley/content/override.py | 7 + worlds/stardew_valley/content/unpacking.py | 97 +++ .../content/vanilla/__init__.py | 0 worlds/stardew_valley/content/vanilla/base.py | 172 ++++ .../content/vanilla/ginger_island.py | 81 ++ .../content/vanilla/pelican_town.py | 393 +++++++++ .../content/vanilla/qi_board.py | 36 + .../content/vanilla/the_desert.py | 46 + .../content/vanilla/the_farm.py | 43 + .../content/vanilla/the_mines.py | 35 + worlds/stardew_valley/data/__init__.py | 2 - worlds/stardew_valley/data/artisan.py | 10 + worlds/stardew_valley/data/bundle_data.py | 221 ++++- worlds/stardew_valley/data/craftable_data.py | 163 ++-- worlds/stardew_valley/data/crops.csv | 41 - worlds/stardew_valley/data/crops_data.py | 50 -- worlds/stardew_valley/data/fish_data.py | 149 ++-- worlds/stardew_valley/data/game_item.py | 86 ++ worlds/stardew_valley/data/harvest.py | 66 ++ worlds/stardew_valley/data/items.csv | 123 ++- worlds/stardew_valley/data/locations.csv | 492 +++++++++-- worlds/stardew_valley/data/museum_data.py | 15 +- worlds/stardew_valley/data/recipe_data.py | 36 +- worlds/stardew_valley/data/recipe_source.py | 10 + worlds/stardew_valley/data/requirement.py | 31 + worlds/stardew_valley/data/shop.py | 40 + worlds/stardew_valley/data/skill.py | 9 + worlds/stardew_valley/data/villagers_data.py | 86 +- .../stardew_valley/docs/en_Stardew Valley.md | 101 +-- worlds/stardew_valley/docs/setup_en.md | 16 +- worlds/stardew_valley/early_items.py | 66 +- worlds/stardew_valley/items.py | 298 ++++--- worlds/stardew_valley/locations.py | 214 +++-- worlds/stardew_valley/logic/ability_logic.py | 1 + worlds/stardew_valley/logic/action_logic.py | 16 +- worlds/stardew_valley/logic/artisan_logic.py | 62 +- worlds/stardew_valley/logic/base_logic.py | 10 +- worlds/stardew_valley/logic/book_logic.py | 24 + worlds/stardew_valley/logic/buff_logic.py | 23 - worlds/stardew_valley/logic/building_logic.py | 12 +- worlds/stardew_valley/logic/bundle_logic.py | 36 +- worlds/stardew_valley/logic/combat_logic.py | 17 +- worlds/stardew_valley/logic/cooking_logic.py | 10 +- worlds/stardew_valley/logic/crafting_logic.py | 22 +- worlds/stardew_valley/logic/crop_logic.py | 72 -- worlds/stardew_valley/logic/farming_logic.py | 59 +- worlds/stardew_valley/logic/fishing_logic.py | 71 +- worlds/stardew_valley/logic/grind_logic.py | 74 ++ .../stardew_valley/logic/harvesting_logic.py | 56 ++ worlds/stardew_valley/logic/has_logic.py | 39 +- worlds/stardew_valley/logic/logic.py | 438 +++++----- .../logic/logic_and_mods_design.md | 19 +- worlds/stardew_valley/logic/logic_event.py | 33 + worlds/stardew_valley/logic/mine_logic.py | 17 +- worlds/stardew_valley/logic/money_logic.py | 35 +- worlds/stardew_valley/logic/monster_logic.py | 25 +- worlds/stardew_valley/logic/museum_logic.py | 21 +- worlds/stardew_valley/logic/pet_logic.py | 33 +- worlds/stardew_valley/logic/quality_logic.py | 33 + worlds/stardew_valley/logic/quest_logic.py | 20 +- worlds/stardew_valley/logic/received_logic.py | 17 +- worlds/stardew_valley/logic/region_logic.py | 10 +- .../logic/relationship_logic.py | 191 +++-- .../stardew_valley/logic/requirement_logic.py | 52 ++ worlds/stardew_valley/logic/season_logic.py | 29 +- worlds/stardew_valley/logic/shipping_logic.py | 6 +- worlds/stardew_valley/logic/skill_logic.py | 67 +- worlds/stardew_valley/logic/source_logic.py | 106 +++ .../logic/special_order_logic.py | 80 +- worlds/stardew_valley/logic/time_logic.py | 90 +- worlds/stardew_valley/logic/tool_logic.py | 21 +- .../mods/logic/deepwoods_logic.py | 14 +- .../stardew_valley/mods/logic/item_logic.py | 95 +-- .../mods/logic/mod_skills_levels.py | 13 +- .../stardew_valley/mods/logic/quests_logic.py | 4 +- .../stardew_valley/mods/logic/skills_logic.py | 38 +- .../mods/logic/special_orders_logic.py | 7 +- worlds/stardew_valley/mods/logic/sve_logic.py | 25 +- worlds/stardew_valley/mods/mod_data.py | 11 - worlds/stardew_valley/mods/mod_regions.py | 46 +- worlds/stardew_valley/option_groups.py | 125 +-- worlds/stardew_valley/options.py | 165 +++- worlds/stardew_valley/presets.py | 65 +- worlds/stardew_valley/region_classes.py | 34 +- worlds/stardew_valley/regions.py | 363 ++++---- worlds/stardew_valley/requirements.txt | 1 + worlds/stardew_valley/rules.py | 320 +++++-- .../scripts/export_locations.py | 8 +- worlds/stardew_valley/stardew_rule/base.py | 101 ++- .../stardew_rule/indirect_connection.py | 22 +- worlds/stardew_valley/stardew_rule/literal.py | 6 - .../stardew_valley/stardew_rule/protocol.py | 4 - .../stardew_rule/rule_explain.py | 164 ++++ worlds/stardew_valley/stardew_rule/state.py | 41 +- .../strings/ap_names/ap_option_names.py | 16 + .../strings/ap_names/buff_names.py | 12 +- .../ap_names/community_upgrade_names.py | 2 + .../strings/ap_names/event_names.py | 24 +- .../strings/ap_names/mods/mod_items.py | 10 +- .../strings/artisan_good_names.py | 39 + worlds/stardew_valley/strings/book_names.py | 65 ++ worlds/stardew_valley/strings/bundle_names.py | 164 ++-- .../stardew_valley/strings/craftable_names.py | 33 +- worlds/stardew_valley/strings/crop_names.py | 97 +-- .../stardew_valley/strings/currency_names.py | 3 + .../stardew_valley/strings/entrance_names.py | 53 +- .../strings/festival_check_names.py | 43 + worlds/stardew_valley/strings/fish_names.py | 166 ++-- worlds/stardew_valley/strings/food_names.py | 12 + .../strings/forageable_names.py | 42 +- .../stardew_valley/strings/machine_names.py | 8 + .../stardew_valley/strings/material_names.py | 1 + worlds/stardew_valley/strings/metal_names.py | 1 + .../strings/monster_drop_names.py | 4 + worlds/stardew_valley/strings/quest_names.py | 4 +- worlds/stardew_valley/strings/region_names.py | 47 +- worlds/stardew_valley/strings/season_names.py | 1 + worlds/stardew_valley/strings/seed_names.py | 49 +- worlds/stardew_valley/strings/skill_names.py | 2 + worlds/stardew_valley/strings/tool_names.py | 1 + .../strings/wallet_item_names.py | 1 + worlds/stardew_valley/test/TestBooksanity.py | 207 +++++ worlds/stardew_valley/test/TestBundles.py | 65 +- worlds/stardew_valley/test/TestData.py | 45 +- worlds/stardew_valley/test/TestFarmType.py | 31 + worlds/stardew_valley/test/TestFill.py | 30 + worlds/stardew_valley/test/TestFishsanity.py | 405 +++++++++ .../stardew_valley/test/TestFriendsanity.py | 159 ++++ worlds/stardew_valley/test/TestGeneration.py | 441 +--------- worlds/stardew_valley/test/TestItems.py | 77 +- worlds/stardew_valley/test/TestLogic.py | 111 +-- .../test/TestMultiplePlayers.py | 2 +- .../test/TestNumberLocations.py | 98 +++ worlds/stardew_valley/test/TestOptions.py | 200 +++-- .../stardew_valley/test/TestOptionsPairs.py | 2 +- worlds/stardew_valley/test/TestRegions.py | 13 +- worlds/stardew_valley/test/TestRules.py | 797 ------------------ worlds/stardew_valley/test/TestStardewRule.py | 83 +- .../stardew_valley/test/TestStartInventory.py | 8 +- .../stardew_valley/test/TestWalnutsanity.py | 209 +++++ worlds/stardew_valley/test/__init__.py | 434 ++++++---- .../test/assertion/mod_assert.py | 6 +- .../test/assertion/option_assert.py | 8 +- .../test/assertion/rule_assert.py | 46 +- .../test/assertion/rule_explain.py | 102 --- .../test/assertion/world_assert.py | 2 +- .../test/content/TestArtisanEquipment.py | 54 ++ .../test/content/TestGingerIsland.py | 55 ++ .../test/content/TestPelicanTown.py | 112 +++ .../test/content/TestQiBoard.py | 27 + .../stardew_valley/test/content/__init__.py | 23 + .../test/content/feature/TestFriendsanity.py | 33 + .../test/content/feature/__init__.py | 0 .../test/content/mods/TestDeepwoods.py | 14 + .../test/content/mods/TestJasper.py | 27 + .../test/content/mods/TestSVE.py | 143 ++++ .../test/content/mods/__init__.py | 0 .../stardew_valley/test/long/TestModsLong.py | 44 +- .../test/long/TestOptionsLong.py | 20 +- .../test/long/TestPreRolledRandomness.py | 5 +- .../test/long/TestRandomWorlds.py | 12 +- .../stardew_valley/test/mods/TestModFish.py | 226 ----- .../test/mods/TestModVillagers.py | 132 --- worlds/stardew_valley/test/mods/TestMods.py | 56 +- .../test/performance/TestPerformance.py | 70 +- .../stardew_valley/test/rules/TestArcades.py | 97 +++ .../test/rules/TestBuildings.py | 62 ++ .../stardew_valley/test/rules/TestBundles.py | 66 ++ .../test/rules/TestCookingRecipes.py | 83 ++ .../test/rules/TestCraftingRecipes.py | 123 +++ .../test/rules/TestDonations.py | 73 ++ .../test/rules/TestFriendship.py | 58 ++ .../stardew_valley/test/rules/TestMuseum.py | 16 + .../stardew_valley/test/rules/TestShipping.py | 82 ++ .../stardew_valley/test/rules/TestSkills.py | 40 + .../test/rules/TestStateRules.py | 12 + worlds/stardew_valley/test/rules/TestTools.py | 141 ++++ .../stardew_valley/test/rules/TestWeapons.py | 75 ++ worlds/stardew_valley/test/rules/__init__.py | 0 worlds/stardew_valley/test/script/__init__.py | 0 .../test/script/benchmark_locations.py | 140 +++ .../test/stability/StabilityOutputScript.py | 4 +- .../test/stability/TestStability.py | 12 +- 210 files changed, 10308 insertions(+), 4550 deletions(-) create mode 100644 worlds/stardew_valley/content/__init__.py create mode 100644 worlds/stardew_valley/content/content_packs.py create mode 100644 worlds/stardew_valley/content/feature/__init__.py create mode 100644 worlds/stardew_valley/content/feature/booksanity.py create mode 100644 worlds/stardew_valley/content/feature/cropsanity.py create mode 100644 worlds/stardew_valley/content/feature/fishsanity.py create mode 100644 worlds/stardew_valley/content/feature/friendsanity.py create mode 100644 worlds/stardew_valley/content/game_content.py create mode 100644 worlds/stardew_valley/content/mod_registry.py create mode 100644 worlds/stardew_valley/content/mods/__init__.py create mode 100644 worlds/stardew_valley/content/mods/archeology.py create mode 100644 worlds/stardew_valley/content/mods/big_backpack.py create mode 100644 worlds/stardew_valley/content/mods/boarding_house.py create mode 100644 worlds/stardew_valley/content/mods/deepwoods.py create mode 100644 worlds/stardew_valley/content/mods/distant_lands.py create mode 100644 worlds/stardew_valley/content/mods/jasper.py create mode 100644 worlds/stardew_valley/content/mods/magic.py create mode 100644 worlds/stardew_valley/content/mods/npc_mods.py create mode 100644 worlds/stardew_valley/content/mods/skill_mods.py create mode 100644 worlds/stardew_valley/content/mods/skull_cavern_elevator.py create mode 100644 worlds/stardew_valley/content/mods/sve.py create mode 100644 worlds/stardew_valley/content/mods/tractor.py create mode 100644 worlds/stardew_valley/content/override.py create mode 100644 worlds/stardew_valley/content/unpacking.py create mode 100644 worlds/stardew_valley/content/vanilla/__init__.py create mode 100644 worlds/stardew_valley/content/vanilla/base.py create mode 100644 worlds/stardew_valley/content/vanilla/ginger_island.py create mode 100644 worlds/stardew_valley/content/vanilla/pelican_town.py create mode 100644 worlds/stardew_valley/content/vanilla/qi_board.py create mode 100644 worlds/stardew_valley/content/vanilla/the_desert.py create mode 100644 worlds/stardew_valley/content/vanilla/the_farm.py create mode 100644 worlds/stardew_valley/content/vanilla/the_mines.py create mode 100644 worlds/stardew_valley/data/artisan.py delete mode 100644 worlds/stardew_valley/data/crops.csv delete mode 100644 worlds/stardew_valley/data/crops_data.py create mode 100644 worlds/stardew_valley/data/game_item.py create mode 100644 worlds/stardew_valley/data/harvest.py create mode 100644 worlds/stardew_valley/data/requirement.py create mode 100644 worlds/stardew_valley/data/shop.py create mode 100644 worlds/stardew_valley/data/skill.py create mode 100644 worlds/stardew_valley/logic/book_logic.py delete mode 100644 worlds/stardew_valley/logic/buff_logic.py delete mode 100644 worlds/stardew_valley/logic/crop_logic.py create mode 100644 worlds/stardew_valley/logic/grind_logic.py create mode 100644 worlds/stardew_valley/logic/harvesting_logic.py create mode 100644 worlds/stardew_valley/logic/logic_event.py create mode 100644 worlds/stardew_valley/logic/quality_logic.py create mode 100644 worlds/stardew_valley/logic/requirement_logic.py create mode 100644 worlds/stardew_valley/logic/source_logic.py create mode 100644 worlds/stardew_valley/stardew_rule/rule_explain.py create mode 100644 worlds/stardew_valley/strings/ap_names/ap_option_names.py create mode 100644 worlds/stardew_valley/strings/book_names.py create mode 100644 worlds/stardew_valley/test/TestBooksanity.py create mode 100644 worlds/stardew_valley/test/TestFarmType.py create mode 100644 worlds/stardew_valley/test/TestFill.py create mode 100644 worlds/stardew_valley/test/TestFishsanity.py create mode 100644 worlds/stardew_valley/test/TestFriendsanity.py create mode 100644 worlds/stardew_valley/test/TestNumberLocations.py delete mode 100644 worlds/stardew_valley/test/TestRules.py create mode 100644 worlds/stardew_valley/test/TestWalnutsanity.py delete mode 100644 worlds/stardew_valley/test/assertion/rule_explain.py create mode 100644 worlds/stardew_valley/test/content/TestArtisanEquipment.py create mode 100644 worlds/stardew_valley/test/content/TestGingerIsland.py create mode 100644 worlds/stardew_valley/test/content/TestPelicanTown.py create mode 100644 worlds/stardew_valley/test/content/TestQiBoard.py create mode 100644 worlds/stardew_valley/test/content/__init__.py create mode 100644 worlds/stardew_valley/test/content/feature/TestFriendsanity.py create mode 100644 worlds/stardew_valley/test/content/feature/__init__.py create mode 100644 worlds/stardew_valley/test/content/mods/TestDeepwoods.py create mode 100644 worlds/stardew_valley/test/content/mods/TestJasper.py create mode 100644 worlds/stardew_valley/test/content/mods/TestSVE.py create mode 100644 worlds/stardew_valley/test/content/mods/__init__.py delete mode 100644 worlds/stardew_valley/test/mods/TestModFish.py delete mode 100644 worlds/stardew_valley/test/mods/TestModVillagers.py create mode 100644 worlds/stardew_valley/test/rules/TestArcades.py create mode 100644 worlds/stardew_valley/test/rules/TestBuildings.py create mode 100644 worlds/stardew_valley/test/rules/TestBundles.py create mode 100644 worlds/stardew_valley/test/rules/TestCookingRecipes.py create mode 100644 worlds/stardew_valley/test/rules/TestCraftingRecipes.py create mode 100644 worlds/stardew_valley/test/rules/TestDonations.py create mode 100644 worlds/stardew_valley/test/rules/TestFriendship.py create mode 100644 worlds/stardew_valley/test/rules/TestMuseum.py create mode 100644 worlds/stardew_valley/test/rules/TestShipping.py create mode 100644 worlds/stardew_valley/test/rules/TestSkills.py create mode 100644 worlds/stardew_valley/test/rules/TestStateRules.py create mode 100644 worlds/stardew_valley/test/rules/TestTools.py create mode 100644 worlds/stardew_valley/test/rules/TestWeapons.py create mode 100644 worlds/stardew_valley/test/rules/__init__.py create mode 100644 worlds/stardew_valley/test/script/__init__.py create mode 100644 worlds/stardew_valley/test/script/benchmark_locations.py 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}") From c96c554dfa68a1b1818031a1049961a25e205621 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:51:10 +0200 Subject: [PATCH 042/393] Tests, WebHost: add tests for host_room and minor cleanup (#3619) * Tests, WebHost: move out setUp and fix typing in api_generate Also fixes a typo and changes client to be per-test rather than a ClassVar * Tests, WebHost: add tests for display_log endpoint * Tests, WebHost: add tests for host_room endpoint * Tests, WebHost: enable Flask DEBUG mode for tests This provides the actual error if a test raised an exception on the server. * Tests, WebHost: use user_path for logs This is what custom_server does now. * Tests, WebHost: avoid triggering security scans --- test/webhost/__init__.py | 36 ++++++ test/webhost/test_api_generate.py | 25 +--- test/webhost/test_host_room.py | 192 ++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 test/webhost/test_host_room.py 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)) From 8c861390668fa02282dd5a67e24a149a65318f79 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:15:29 -0400 Subject: [PATCH 043/393] ALTTP: Bombable Wall to Crystaroller Room Logic (#3627) --- worlds/alttp/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 171c82f9b226..67684a6f3ced 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -488,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)) From 1e3a4b6db5acff8e82bdecdc79773f2923de919a Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 10 Jul 2024 23:11:47 -0700 Subject: [PATCH 044/393] Zillion: more rooms added to map_gen option (#3634) --- worlds/zillion/client.py | 5 +++++ worlds/zillion/gen_data.py | 7 +++++++ worlds/zillion/requirements.txt | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) 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/requirements.txt b/worlds/zillion/requirements.txt index b4f554902f48..d6b01ac107ae 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4a2fec0aa1c529df866e510cdfcf6dca4d53679b#0.8.0 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 typing-extensions>=4.7, <5 From eaec41d8854ef53bf626a1dce4f81a87cd867500 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 11 Jul 2024 16:44:29 -0400 Subject: [PATCH 045/393] TUNIC: Fix event region for Quarry fuse (#3635) --- worlds/tunic/er_scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 8689a51b7601..0bd8c5e80681 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -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", From 187f9dac9425b916f2f60cd673f1acf31390fc69 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:56:27 +0200 Subject: [PATCH 046/393] customserver: preemtively run GC before starting room (#3637) GC seems to be lazy. --- WebHostLib/customserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b61e5..50c316f3b750 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -325,6 +325,7 @@ 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) From 948f50f35db1b13336f08b0e5740d900904404b0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:56:56 +0200 Subject: [PATCH 047/393] customserver: fix minor memory leak (#3636) Old code keeps ref to last started room's task and thus never fully cleans it up. --- WebHostLib/customserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 50c316f3b750..ccffc40b384d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -330,6 +330,7 @@ def run(self): 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 From 48dc14421e125d20d651f8b0368b20b484250631 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 14 Jul 2024 05:05:50 -0700 Subject: [PATCH 048/393] Pokemon Emerald: Fix logic for coin case location (#3631) --- worlds/pokemon_emerald/rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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( From 08a36ec223b1c7bc27cca3b766744fcbdd844326 Mon Sep 17 00:00:00 2001 From: dennisw100 <100dennisw@gmail.com> Date: Sun, 14 Jul 2024 14:11:52 +0200 Subject: [PATCH 049/393] Undertale: Fixed output location of the patched game in UndertaleClient.py (#3418) * Update UndertaleClient.py Fixed output location of the patched game Fixed the error that when the client is opened outside of the archipelago folder, the patched folder would be created in there which on windows ends up trying to create it in the system32 folder Bug Report: https://discord.com/channels/731205301247803413/1148330675452264499/1237412436382973962 * Undertale: removed unnecessary wrapping in UndertaleClient.py I did not know os.path.join was unnecessary in this case the more you know. --- UndertaleClient.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 415d7e7f21a3..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"]) From e76d32e9089efa0554034a0c2c9049f03026267e Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sun, 14 Jul 2024 08:17:05 -0400 Subject: [PATCH 050/393] AHIT: Fix act shuffle test fail (#3522) --- worlds/ahit/Regions.py | 3 +++ worlds/ahit/Rules.py | 3 +++ 2 files changed, 6 insertions(+) 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")) From 925e02dca72420d94971db28c2c15c4ddd3092c2 Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Mon, 15 Jul 2024 06:09:02 -0700 Subject: [PATCH 051/393] Raft: Move to new Options API (#3587) --- worlds/raft/Options.py | 31 ++++++++++++++------------ worlds/raft/Rules.py | 4 ++-- worlds/raft/__init__.py | 48 ++++++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index 696d4dbab477..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""" @@ -47,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.""" @@ -76,16 +79,16 @@ class PaddleboardMode(Toggle): """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): From 7b39b23f73d2d13ac2d859ad546308f216041693 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Jul 2024 22:33:51 +0200 Subject: [PATCH 052/393] Subnautica: increase minimum client version (#3657) --- worlds/subnautica/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 856117469e55..58d8fa543a6d 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - required_client_version = (0, 4, 1) + required_client_version = (0, 5, 0) creatures_to_scan: List[str] From 4d1507cd0e1a7cc24b7564b5dfee84209c76a9d0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 18 Jul 2024 00:49:59 +0200 Subject: [PATCH 053/393] Core: Update cx_freeze to 7.2.0 and freeze it (#3648) supersedes ArchipelagoMW/Archipelago#3405 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85c0f9f7ff13..cb4d1a7511b6 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze==7.0.0' + requirement = 'cx-Freeze==7.2.0' import pkg_resources try: pkg_resources.require(requirement) From e33a9991ef381b27ed09a075925254b3f7620527 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Fri, 19 Jul 2024 01:37:59 -0500 Subject: [PATCH 054/393] CommonClient: Escape markup sent in chat messages (#3659) * escape markup in uncolored text * Fix comment to allign with style guide Fixes the comment so it follows the style guide, along with making it better explain the code. * Make more concise --- kvui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvui.py b/kvui.py index 500203a8818f..1409e2dc0d45 100644 --- a/kvui.py +++ b/kvui.py @@ -836,6 +836,10 @@ def _handle_color(self, node: JSONMessagePart): return self._handle_text(node) def _handle_text(self, node: JSONMessagePart): + # All other text goes through _handle_color, and we don't want to escape markup twice, + # or mess up text that already has intentional markup applied to it + if node.get("type", "text") == "text": + node["text"] = escape_markup(node["text"]) for ref in node.get("refs", []): node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" self.ref_count += 1 From 34e7748f23083d26b126a52ca447afe3d93ff4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:24:24 -0400 Subject: [PATCH 055/393] Stardew Valley: Make sure number of month in time logic is a int to improve performance by ~20% (#3665) * make sure number of month is actually a int * improve rule explain like in pr * remove redundant if in can_complete_bundle * assert number is int so cache is not bloated --- worlds/stardew_valley/logic/bundle_logic.py | 6 ++-- worlds/stardew_valley/logic/museum_logic.py | 2 +- worlds/stardew_valley/logic/time_logic.py | 2 ++ worlds/stardew_valley/stardew_rule/base.py | 2 +- .../stardew_rule/rule_explain.py | 33 +++++++++++++++++-- worlds/stardew_valley/stardew_rule/state.py | 2 +- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 4ca5fd81fc76..98fda1c73c7d 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -27,8 +27,8 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, -QuestLogicMixin]]): +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 = [] @@ -45,7 +45,7 @@ def can_complete_bundle(self, bundle: Bundle) -> StardewRule: qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + time_rule = 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: diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 4ba5364f5524..36ba62b31fcb 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -41,7 +41,7 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - time_needed_to_grind = (20 - item.difficulty) / 2 + time_needed_to_grind = int((20 - item.difficulty) // 2) time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 94e0e277c86c..2ba76579ff45 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -26,8 +26,10 @@ class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): @cache_self1 def has_lived_months(self, number: int) -> StardewRule: + assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing." if number <= 0: return self.logic.true_ + number = min(number, MAX_MONTHS) return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 576cd36851fb..3e6eb327ea99 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -431,7 +431,7 @@ def rules_count(self): return len(self.rules) def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" + return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]" @dataclass(frozen=True) diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py index 61a88ceb6996..a9767c7b72d5 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -34,7 +34,7 @@ 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) + return self.summary(depth) + "\n" + "\n".join(i.__str__(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)) @@ -42,7 +42,7 @@ 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) + return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @cached_property @@ -61,6 +61,33 @@ def explained_sub_rules(self) -> List[RuleExplanation]: return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] +@dataclass +class CountSubRuleExplanation(RuleExplanation): + count: int = 1 + + @staticmethod + def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation: + return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count) + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + +@dataclass +class CountExplanation(RuleExplanation): + rule: Count + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + return [ + CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count) + for rule, count in self.rule.counter.items() + ] + + def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: if isinstance(rule, StardewRule): return _explain(rule, state, expected, explored_spots=set()) @@ -80,7 +107,7 @@ def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, expl @_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) + return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) @_explain.register diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index cf0996a63bbc..5f5e61b3d4e5 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -122,4 +122,4 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul return self, self(state) def __repr__(self): - return f"Received {self.percent}% progression items." + return f"Received {self.percent}% progression items" From 7039b17bf6e00735b5698e149c5cbce0df729e2b Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 21 Jul 2024 18:12:11 -0500 Subject: [PATCH 056/393] CommonClient: fix bug when using Connect button without a disconnect (#3609) * makes the kivy connect button do the same username forgetting that /connect does to fix an issue where losing connection would make you unable to connect to a different server * extract duplicate code --- kvui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index 1409e2dc0d45..a63d636960a7 100644 --- a/kvui.py +++ b/kvui.py @@ -595,8 +595,8 @@ def command_button_action(self, button): "!help for server commands.") def connect_button_action(self, button): + self.ctx.username = None if self.ctx.server: - self.ctx.username = None async_start(self.ctx.disconnect()) else: async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", ""))) From d7d45654290f0c04e9f601c097d8c1d160a18908 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:27:10 +0200 Subject: [PATCH 057/393] YGO06: fixes non-deterministic bug by changing sets to lists (#3674) --- worlds/yugioh06/boosterpacks.py | 186 +++++++++++++++--------------- worlds/yugioh06/structure_deck.py | 50 ++++---- 2 files changed, 120 insertions(+), 116 deletions(-) diff --git a/worlds/yugioh06/boosterpacks.py b/worlds/yugioh06/boosterpacks.py index f6f4ec7732c3..645977d28def 100644 --- a/worlds/yugioh06/boosterpacks.py +++ b/worlds/yugioh06/boosterpacks.py @@ -1,13 +1,13 @@ -from typing import Dict, Set +from typing import Dict, List -booster_contents: Dict[str, Set[str]] = { - "LEGEND OF B.E.W.D.": { +booster_contents: Dict[str, List[str]] = { + "LEGEND OF B.E.W.D.": [ "Exodia", "Dark Magician", "Polymerization", "Skull Servant" - }, - "METAL RAIDERS": { + ], + "METAL RAIDERS": [ "Petit Moth", "Cocoon of Evolution", "Time Wizard", @@ -30,8 +30,8 @@ "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ "Senju of the Thousand Hands", "Sonic Bird", "Mystical Space Typhoon" - }, - "LABYRINTH OF NIGHTMARE": { + ], + "LABYRINTH OF NIGHTMARE": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -119,8 +119,8 @@ "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ "Spell Reproduction", "Contract with the Abyss", "Dark Master - Zorc" - }, - "INVASION OF CHAOS": { + ], + "INVASION OF CHAOS": [ "Ojama Delta Hurricane", "Ojama Yellow", "Ojama Black", @@ -241,8 +241,8 @@ "Cursed Seal of the Forbidden Spell", "Stray Lambs", "Manju of the Ten Thousand Hands" - }, - "ANCIENT SANCTUARY": { + ], + "ANCIENT SANCTUARY": [ "Monster Gate", "Wall of Revealing Light", "Mystik Wok", @@ -255,8 +255,8 @@ "King of the Swamp", "Enemy Controller", "Enchanting Fitting Room" - }, - "SOUL OF THE DUELIST": { + ], + "SOUL OF THE DUELIST": [ "Ninja Grandmaster Sasuke", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -272,8 +272,8 @@ "Level Up!", "Howling Insect", "Mobius the Frost Monarch" - }, - "RISE OF DESTINY": { + ], + "RISE OF DESTINY": [ "Homunculus the Alchemic Being", "Thestalos the Firestorm Monarch", "Roc from the Valley of Haze", @@ -283,8 +283,8 @@ "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ "Elemental Hero Thunder Giant", "Aussa the Earth Charmer", "Brain Control" - }, - "CYBERNETIC REVOLUTION": { + ], + "CYBERNETIC REVOLUTION": [ "Power Bond", "Cyber Dragon", "Cyber Twin Dragon", @@ -322,8 +322,8 @@ "Miracle Fusion", "Elemental Hero Bubbleman", "Jerry Beans Man" - }, - "ELEMENTAL ENERGY": { + ], + "ELEMENTAL ENERGY": [ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult", @@ -344,8 +344,8 @@ "Elemental Hero Bladedge", "Pot of Avarice", "B.E.S. Tetran" - }, - "SHADOW OF INFINITY": { + ], + "SHADOW OF INFINITY": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -357,8 +357,8 @@ "Gokipon", "Demise, King of Armageddon", "Anteatereatingant" - }, - "GAME GIFT COLLECTION": { + ], + "GAME GIFT COLLECTION": [ "Ritual", "Valkyrion the Magna Warrior", "Alpha the Magnet Warrior", @@ -383,8 +383,8 @@ "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ "Curse of Vampire", "Elemental Hero Flame Wingman", "Magician of Black Chaos" - }, - "Fairy Collection": { + ], + "Fairy Collection": [ "Silpheed", "Dunames Dark Witch", "Hysteric Fairy", @@ -416,8 +416,8 @@ "Asura Priest", "Manju of the Ten Thousand Hands", "Senju of the Thousand Hands" - }, - "Dragon Collection": { + ], + "Dragon Collection": [ "Victory D.", "Chaos Emperor Dragon - Envoy of the End", "Kaiser Glider", @@ -434,16 +434,16 @@ "Troop Dragon", "Horus the Black Flame Dragon LV4", "Pitch-Dark Dragon" - }, - "Warrior Collection A": { + ], + "Warrior Collection A": [ "Gate Guardian", "Gearfried the Iron Knight", "Dimensional Warrior", "Command Knight", "The Last Warrior from Another Planet", "Dream Clown" - }, - "Warrior Collection B": { + ], + "Warrior Collection B": [ "Don Zaloog", "Dark Scorpion - Chick the Yellow", "Dark Scorpion - Meanae the Thorn", @@ -467,8 +467,8 @@ "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ "Spear Cretin", "Versago the Destroyer", "Toon Summoned Skull" - }, - "Fiend Collection B": { + ], + "Fiend Collection B": [ "Raviel, Lord of Phantasms", "Yata-Garasu", "Helpoemer", @@ -505,15 +505,15 @@ "Jowls of Dark Demise", "D. D. Trainer", "Earthbound Spirit" - }, - "Machine Collection A": { + ], + "Machine Collection A": [ "Cyber-Stein", "Mechanicalchaser", "Jinzo", "UFO Turtle", "Cyber-Tech Alligator" - }, - "Machine Collection B": { + ], + "Machine Collection B": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -531,8 +531,8 @@ "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ "Injection Fairy Lily", "Cosmo Queen", "Magician of Black Chaos" - }, - "Spellcaster Collection B": { + ], + "Spellcaster Collection B": [ "Jowgen the Spiritualist", "Tsukuyomi", "Manticore of Darkness", @@ -574,8 +574,8 @@ "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ "Des Lacooda", "Wandering Mummy", "Royal Keeper" - }, - "Special Monsters A": { + ], + "Special Monsters A": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -626,8 +626,8 @@ "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ "Level Up!", "Ultimate Insect Lv3", "Ultimate Insect Lv5" - }, - "Reverse Collection": { + ], + "Reverse Collection": [ "Magical Merchant", "Castle of Dark Illusions", "Magician of Faith", @@ -675,8 +675,8 @@ "Spear Cretin", "Nobleman of Crossout", "Aussa the Earth Charmer" - }, - "LP Recovery Collection": { + ], + "LP Recovery Collection": [ "Mystik Wok", "Poison of the Old Man", "Hysteric Fairy", @@ -691,8 +691,8 @@ "Elemental Hero Steam Healer", "Fushi No Tori", "Emergency Provisions" - }, - "Special Summon Collection A": { + ], + "Special Summon Collection A": [ "Perfectly Ultimate Great Moth", "Dark Sage", "Polymerization", @@ -726,8 +726,8 @@ "Morphing Jar #2", "Spear Cretin", "Dark Magic Curtain" - }, - "Special Summon Collection B": { + ], + "Special Summon Collection B": [ "Monster Gate", "Chaos Emperor Dragon - Envoy of the End", "Ojama Trio", @@ -756,8 +756,8 @@ "Tribute Doll", "Enchanting Fitting Room", "Stray Lambs" - }, - "Special Summon Collection C": { + ], + "Special Summon Collection C": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -782,13 +782,13 @@ "Ultimate Insect Lv5", "Rescue Cat", "Anteatereatingant" - }, - "Equipment Collection": { + ], + "Equipment Collection": [ "Megamorph", "Cestus of Dagla", "United We Stand" - }, - "Continuous Spell/Trap A": { + ], + "Continuous Spell/Trap A": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -801,8 +801,8 @@ "Solemn Wishes", "Embodiment of Apophis", "Toon World" - }, - "Continuous Spell/Trap B": { + ], + "Continuous Spell/Trap B": [ "Hamon, Lord of Striking Thunder", "Uria, Lord of Searing Flames", "Wave-Motion Cannon", @@ -815,8 +815,8 @@ "Skull Zoma", "Pitch-Black Power Stone", "Metal Reflect Slime" - }, - "Quick/Counter Collection": { + ], + "Quick/Counter Collection": [ "Mystik Wok", "Poison of the Old Man", "Scapegoat", @@ -841,8 +841,8 @@ "Book of Moon", "Serial Spell", "Mystical Space Typhoon" - }, - "Direct Damage Collection": { + ], + "Direct Damage Collection": [ "Hamon, Lord of Striking Thunder", "Chaos Emperor Dragon - Envoy of the End", "Dark Snake Syndrome", @@ -868,8 +868,8 @@ "Jowls of Dark Demise", "Stealth Bird", "Elemental Hero Bladedge", - }, - "Direct Attack Collection": { + ], + "Direct Attack Collection": [ "Victory D.", "Dark Scorpion Combination", "Spirit Reaper", @@ -880,8 +880,8 @@ "Toon Mermaid", "Toon Summoned Skull", "Toon Dark Magician Girl" - }, - "Monster Destroy Collection": { + ], + "Monster Destroy Collection": [ "Hamon, Lord of Striking Thunder", "Inferno", "Ninja Grandmaster Sasuke", @@ -912,12 +912,12 @@ "Offerings to the Doomed", "Divine Wrath", "Dream Clown" - }, + ], } def get_booster_locations(booster: str) -> Dict[str, str]: return { f"{booster} {i}": content - for i, content in enumerate(booster_contents[booster]) + for i, content in enumerate(booster_contents[booster], 1) } diff --git a/worlds/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py index d58223f2e216..3559e7c5153e 100644 --- a/worlds/yugioh06/structure_deck.py +++ b/worlds/yugioh06/structure_deck.py @@ -1,7 +1,7 @@ -from typing import Dict, Set +from typing import Dict, List -structure_contents: Dict[str, Set] = { - "dragons_roar": { +structure_contents: Dict[str, List[str]] = { + "dragons_roar": [ "Luster Dragon", "Armed Dragon LV3", "Armed Dragon LV5", @@ -14,9 +14,9 @@ "Stamping Destruction", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "zombie_madness": { + "Mystical Space Typhoon" + ], + "zombie_madness": [ "Pyramid Turtle", "Regenerating Mummy", "Ryu Kokki", @@ -26,9 +26,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "blazing_destruction": { + "Mystical Space Typhoon" + ], + "blazing_destruction": [ "Inferno", "Solar Flare Dragon", "UFO Turtle", @@ -38,9 +38,9 @@ "Level Limit - Area B", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "fury_from_the_deep": { + "Mystical Space Typhoon" + ], + "fury_from_the_deep": [ "Mother Grizzly", "Water Beaters", "Gravity Bind", @@ -48,9 +48,9 @@ "Mobius the Frost Monarch", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "warriors_triumph": { + "Mystical Space Typhoon" + ], + "warriors_triumph": [ "Gearfried the Iron Knight", "D.D. Warrior Lady", "Marauding Captain", @@ -60,9 +60,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "spellcasters_judgement": { + "Mystical Space Typhoon" + ], + "spellcasters_judgement": [ "Dark Magician", "Apprentice Magician", "Breaker the Magical Warrior", @@ -70,14 +70,18 @@ "Skilled Dark Magician", "Tsukuyomi", "Magical Dimension", - "Mage PowerSpell-Counter Cards", + "Mage Power", + "Spell-Counter Cards", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "none": {}, + "Mystical Space Typhoon" + ], + "none": [], } def get_deck_content_locations(deck: str) -> Dict[str, str]: - return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])} + return { + f"{deck} {i}": content + for i, content in enumerate(structure_contents[deck], 1) + } From 12f1ef873c2442ae923fd0585c2252c6033c1075 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:47:46 -0500 Subject: [PATCH 058/393] A Short Hike: Fix Boat Rental purchase being incorrectly calculated (#3639) --- worlds/shorthike/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py index 319ad8f20e1b..657035a03011 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ class LocationInfo(TypedDict): {"name": "Boat Rental", "id": base_id + 55, "inGameId": "DadDeer[0]", - "needsShovel": False, "purchase": True, + "needsShovel": False, "purchase": 100, "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, {"name": "Boat Challenge Reward", "id": base_id + 56, From 48a0fb05a2e4d5da7727f576e0b54725950b8ee3 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 02:52:44 +0300 Subject: [PATCH 059/393] Stardew Valley: Removed Stardrop Tea from Full Shipment (#3655) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index bb2ed2e2ce1f..0c5a12fb573b 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2221,7 +2221,7 @@ id,region,name,tags,mod_name 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", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY", 3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", 3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", 3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", From e59bec36ec792937d2916f0256c3655cc27a39fa Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 09:32:40 +0300 Subject: [PATCH 060/393] Stardew Valley: Add gourmand frog rules for completing his tasks sequentially (#3652) --- worlds/stardew_valley/rules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index c30d04c8a6f2..62a5cc52181b 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -482,8 +482,10 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): 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("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) 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()) From f7989780fa023d7c8be2ad7ed956db9a52483af9 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:17:34 -0600 Subject: [PATCH 061/393] Bomb Rush Cyberfunk: Fix final graffiti location being unobtainable (#3669) --- worlds/bomb_rush_cyberfunk/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/bomb_rush_cyberfunk/Locations.py b/worlds/bomb_rush_cyberfunk/Locations.py index 863e2ad020c0..7ea959019067 100644 --- a/worlds/bomb_rush_cyberfunk/Locations.py +++ b/worlds/bomb_rush_cyberfunk/Locations.py @@ -762,7 +762,7 @@ class EventDict(TypedDict): 'game_id': "graf385"}, {'name': "Tagged 389 Graffiti Spots", 'stage': Stages.Misc, - 'game_id': "graf379"}, + 'game_id': "graf389"}, ] From c12d3dd6ade3b0d2ad65095e2b7058741376ad91 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 23 Jul 2024 01:36:42 +0300 Subject: [PATCH 062/393] Stardew valley: Fix Queen of Sauce Cookbook conditions (#3651) * - Extracted walnut logic to a Mixin so it can be used in content pack requirements * - Add 100 walnut requirements to the Queen of Sauce Cookbook * - Woops a file wasn't added to previous commits * - Make the queen of sauce cookbook a ginger island only thing, due to the walnut requirement * - Moved the book in the correct content pack * - Removed an empty class that I'm not sure where it came from --- worlds/stardew_valley/__init__.py | 2 +- .../content/vanilla/ginger_island.py | 6 +- .../content/vanilla/pelican_town.py | 3 - worlds/stardew_valley/data/locations.csv | 4 +- worlds/stardew_valley/data/requirement.py | 5 + worlds/stardew_valley/logic/logic.py | 116 +-------------- .../stardew_valley/logic/requirement_logic.py | 9 +- worlds/stardew_valley/logic/walnut_logic.py | 135 ++++++++++++++++++ worlds/stardew_valley/rules.py | 24 ++-- 9 files changed, 171 insertions(+), 133 deletions(-) create mode 100644 worlds/stardew_valley/logic/walnut_logic.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 07235ad2983a..1aba9af7ab56 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -255,7 +255,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[GoalName.greatest_walnut_hunter], - self.logic.has_walnut(130), + self.logic.walnut.has_walnut(130), Event.victory) elif self.options.goal == Goal.option_protector_of_the_valley: self.create_event_location(location_table[GoalName.protector_of_the_valley], diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py index d824deff3903..2fbcb032799e 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -3,6 +3,7 @@ from ...data import villagers_data, fish_data from ...data.game_item import ItemTag, Tag from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.requirement import WalnutRequirement from ...data.shop import ShopSource from ...strings.book_names import Book from ...strings.crop_names import Fruit, Vegetable @@ -10,7 +11,7 @@ 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.region_names import Region, LogicRegion from ...strings.season_names import Season from ...strings.seed_names import Seed @@ -62,6 +63,9 @@ def harvest_source_hook(self, content: StardewContent): Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), ), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever }, fishes=( diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 2c687eacbdde..917e8cca220a 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -290,9 +290,6 @@ 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, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 0c5a12fb573b..6e30d2b8c858 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2252,7 +2252,7 @@ id,region,name,tags,mod_name 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", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND", 3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", 3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", 3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", @@ -2292,7 +2292,7 @@ id,region,name,tags,mod_name 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", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND", 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", diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 7e9466630fc3..4744f9dffdfe 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -29,3 +29,8 @@ class SeasonRequirement(Requirement): @dataclass(frozen=True) class YearRequirement(Requirement): year: int + + +@dataclass(frozen=True) +class WalnutRequirement(Requirement): + amount: int diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 74cdaf2374e1..fb0d938fbb1e 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from functools import cached_property from typing import Collection, Callable from .ability_logic import AbilityLogicMixin @@ -43,6 +42,7 @@ from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin +from .walnut_logic import WalnutLogicMixin from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes from ..data.museum_data import all_museum_items @@ -50,16 +50,14 @@ from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions 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_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 +from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration @@ -96,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -461,32 +459,6 @@ def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - @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") - 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 rules_worth_a_point = [ @@ -566,86 +538,6 @@ def has_island_trader(self) -> StardewRule: return False_() return self.region.can_reach(Region.island_trader) - def has_walnut(self, number: int) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - 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) - reach_west = self.region.can_reach(Region.island_west) - reach_hut = self.region.can_reach(Region.leo_hut) - 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 = 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 = 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 = 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 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 self.logic.and_(*reach_walnut_regions) - if number <= 50: - return reach_entire_island - gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) - return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \ - self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ - self.can_complete_field_office() - def has_all_stardrops(self) -> StardewRule: other_rules = [] number_of_stardrops_to_receive = 0 diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 87d9ee021524..9356440ac6a8 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -9,8 +9,9 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin +from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -20,7 +21,7 @@ def __init__(self, *args, **kwargs): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -50,3 +51,7 @@ def _(self, requirement: SeasonRequirement): @meet_requirement.register def _(self, requirement: YearRequirement): return self.logic.time.has_year(requirement.year) + + @meet_requirement.register + def _(self, requirement: WalnutRequirement): + return self.logic.walnut.has_walnut(requirement.amount) diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py new file mode 100644 index 000000000000..14fe1c339090 --- /dev/null +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -0,0 +1,135 @@ +from functools import cached_property +from typing import Union + +from .ability_logic import AbilityLogicMixin +from .base_logic import BaseLogic, BaseLogicMixin +from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from ..strings.ap_names.event_names import Event +from ..options import ExcludeGingerIsland, Walnutsanity +from ..stardew_rule import StardewRule, False_, True_ +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.craftable_names import Furniture +from ..strings.crop_names import Fruit +from ..strings.metal_names import Mineral, Fossil +from ..strings.region_names import Region +from ..strings.seed_names import Seed + + +class WalnutLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.walnut = WalnutLogic(*args, **kwargs) + + +class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, + AbilityLogicMixin]]): + + def has_walnut(self, number: int) -> StardewRule: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: + 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.logic.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: + # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations + reach_south = self.logic.region.can_reach(Region.island_south) + reach_north = self.logic.region.can_reach(Region.island_north) + reach_west = self.logic.region.can_reach(Region.island_west) + reach_hut = self.logic.region.can_reach(Region.leo_hut) + reach_southeast = self.logic.region.can_reach(Region.island_south_east) + reach_field_office = self.logic.region.can_reach(Region.field_office) + reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) + reach_volcano_regions = [self.logic.region.can_reach(Region.volcano), + self.logic.region.can_reach(Region.volcano_secret_beach), + self.logic.region.can_reach(Region.volcano_floor_5), + self.logic.region.can_reach(Region.volcano_floor_10)] + 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 = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site), + self.logic.region.can_reach(Region.gourmand_frog_cave), + self.logic.region.can_reach(Region.colored_crystals_cave), + self.logic.region.can_reach(Region.shipwreck), self.logic.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 self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) + if number <= 10: + return self.logic.count(2, *reach_walnut_regions) + if number <= 15: + return self.logic.count(3, *reach_walnut_regions) + if number <= 20: + 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) + return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() + + @cached_property + def can_start_field_office(self) -> StardewRule: + field_office = self.logic.region.can_reach(Region.field_office) + professor_snail = self.logic.received("Open Professor Snail Cave") + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.logic.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.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.logic.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() diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 62a5cc52181b..7c1fdbda3cf4 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -375,7 +375,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), - logic.can_complete_field_office()) + logic.walnut.can_complete_field_office()) set_walnut_rules(logic, multiworld, player, world_options) @@ -432,10 +432,10 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): # 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) + has_walnut = logic.walnut.has_walnut(5) + has_5_walnut = logic.walnut.has_walnut(15) + has_10_walnut = logic.walnut.has_walnut(40) + has_20_walnut = logic.walnut.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), @@ -471,7 +471,7 @@ def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: Sta set_walnut_repeatable_rules(logic, multiworld, player, world_options) -def set_walnut_puzzle_rules(logic, multiworld, player, world_options): +def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: return @@ -487,12 +487,12 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) 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("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.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)) From b840c3fe1a609466c3b103a1972d5ec9d862df54 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 22 Jul 2024 18:43:41 -0400 Subject: [PATCH 063/393] TUNIC: Move 3 locations to Quarry Back (#3649) * Move 3 locations to Quarry Back * Change the non-er region too --- worlds/tunic/locations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 2d87140fe50f..09916228163d 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -208,15 +208,15 @@ class TunicLocationData(NamedTuple): "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), - "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), From 9c2933f8033c8e8b9d9acdd341b043d7eca89d76 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 22 Jul 2024 18:45:49 -0400 Subject: [PATCH 064/393] Lingo: Fix Early Color Hallways painting in pilgrimages (#3645) --- worlds/lingo/data/LL1.yaml | 1 - worlds/lingo/data/generated.dat | Bin 136017 -> 136017 bytes worlds/lingo/regions.py | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4d6771a7350d..970063d58542 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -3265,7 +3265,6 @@ door: Traveled Entrance Color Hallways: door: Color Hallways Entrance - warp: True panels: Achievement: id: Countdown Panels/Panel_traveled_traveled diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 6c8c925138aa5ac61edb22d194cfb8e9b9e4a492..065308628b9fc15f0d6001d6d8b30382ba8278d1 100644 GIT binary patch delta 131 zcmcb(jN{@mjtS}tCO}|joR*qqY;0j+WS(edkZ5X{Vqk7*kerreVUe`aJ None: RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) if early_color_hallways: - connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways", + connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", None, EntranceType.PAINTING, False, world) if painting_shuffle: From 51883757367e7ee859442ea2f55d59cc565f1704 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 23 Jul 2024 02:34:47 -0400 Subject: [PATCH 065/393] Lingo: Add pilgrimage logic through Starting Room (#3654) * Lingo: Add pilgrimage logic through Starting Room * Added unit test * Reverse order of two doors in unit test * Remove print statements from TestPilgrimage * Update generated.dat --- worlds/lingo/data/LL1.yaml | 9 ++++++++ worlds/lingo/data/generated.dat | Bin 136017 -> 136277 bytes worlds/lingo/test/TestPilgrimage.py | 32 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 970063d58542..e12ca022973b 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -140,6 +140,15 @@ painting: True The Colorful: painting: True + Welcome Back Area: + room: Welcome Back Area + door: Shortcut to Starting Room + Second Room: + door: Main Door + Hidden Room: + door: Back Right Door + Rhyme Room (Looped Square): + door: Rhyme Room Entrance panels: HI: id: Entry Room/Panel_hi_hi diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 065308628b9fc15f0d6001d6d8b30382ba8278d1..3ed6cb24f7d289c38189932fe5ccf705bdaf0262 100644 GIT binary patch delta 30433 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 Date: Tue, 23 Jul 2024 03:04:24 -0400 Subject: [PATCH 066/393] TUNIC: Add setting to disable local spoiler to host yaml (#3661) * Add TunicSettings class for host yaml options * Update __init__.py * Update worlds/tunic/__init__.py Co-authored-by: Scipio Wright * Use self.settings * Remove unused import --------- Co-authored-by: Scipio Wright --- worlds/tunic/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index f63193e6aeef..9b28d1d451a8 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Tuple, TypedDict +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names @@ -12,6 +12,14 @@ from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP +from settings import Group, Bool + + +class TunicSettings(Group): + class DisableLocalSpoiler(Bool): + """Disallows the TUNIC client from creating a local spoiler log.""" + + disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False class TunicWeb(WebWorld): @@ -57,6 +65,7 @@ class TunicWorld(World): options: TunicOptions options_dataclass = TunicOptions + settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups @@ -373,7 +382,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], "Hexagon Quest Goal": self.options.hexagon_goal.value, - "Entrance Rando": self.tunic_portal_pairs + "Entrance Rando": self.tunic_portal_pairs, + "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): From dc50444edddeba1003c1fc4a76e5cac9fb2e257e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:13:41 +0200 Subject: [PATCH 067/393] The Witness: Small naming inconsistencies (#3618) --- worlds/witness/data/WitnessLogic.txt | 4 ++-- worlds/witness/data/WitnessLogicExpert.txt | 4 ++-- worlds/witness/data/WitnessLogicVanilla.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 272ed176e342..b7814626ada0 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 63e7e36c243e..1d1d010fde88 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 1aa9655361f9..851031ab72f0 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True From ad5089b5a3ed67c326b5fbd036ba4b08bf3477f2 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 24 Jul 2024 07:36:41 -0400 Subject: [PATCH 068/393] DLC Quest - Add option groups to DLC Quest (#3677) * - Add option groups to DLC Quest * - Slight reorganisation * - Add type hint --- worlds/dlcquest/__init__.py | 2 ++ worlds/dlcquest/option_groups.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 worlds/dlcquest/option_groups.py diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index a9dfcc5044b1..2fc0da075d22 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,11 +8,13 @@ from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Archipelago DLCQuest game on your computer.", diff --git a/worlds/dlcquest/option_groups.py b/worlds/dlcquest/option_groups.py new file mode 100644 index 000000000000..9510c061e18f --- /dev/null +++ b/worlds/dlcquest/option_groups.py @@ -0,0 +1,27 @@ +from typing import List + +from Options import ProgressionBalancing, Accessibility, OptionGroup +from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity, + CoinSanityRange, DeathLink) + +dlcq_option_groups: List[OptionGroup] = [ + OptionGroup("General", [ + Campaign, + ItemShuffle, + CoinSanity, + ]), + OptionGroup("Customization", [ + EndingChoice, + PermanentCoins, + CoinSanityRange, + ]), + OptionGroup("Tedious and Grind", [ + TimeIsMoney, + DoubleJumpGlitch, + ]), + OptionGroup("Advanced Options", [ + DeathLink, + ProgressionBalancing, + Accessibility, + ]), +] From e7dbfa7fcd073a50a2b9a1786d6385657d6b8d73 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:46:14 -0400 Subject: [PATCH 069/393] FFMQ: Efficiency Improvement and Use New Options Methods (#2767) * FFMQ Efficiency improvement and use new options methods * Hard check for 0x01 game status * Fixes * Why were Mac's Ship entrance hints excluded? * Two remaining per_slot_randoms purged * reformat generate_early * Utils.parse_yaml --- worlds/ffmq/Client.py | 2 +- worlds/ffmq/Items.py | 20 +- worlds/ffmq/Options.py | 69 +- worlds/ffmq/Output.py | 74 +- worlds/ffmq/Regions.py | 47 +- worlds/ffmq/__init__.py | 62 +- worlds/ffmq/data/entrances.yaml | 2450 ------------------- worlds/ffmq/data/rooms.py | 2 + worlds/ffmq/data/rooms.yaml | 4026 ------------------------------- 9 files changed, 134 insertions(+), 6618 deletions(-) delete mode 100644 worlds/ffmq/data/entrances.yaml create mode 100644 worlds/ffmq/data/rooms.py delete mode 100644 worlds/ffmq/data/rooms.yaml diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 7de486314c6c..6cb35dd3b4be 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ async def game_watcher(self, ctx): received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): + if check_1 != b'01' or check_2 != b'01': return def get_range(data_range): diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index d0898d7e81c8..f1c102d34ef8 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -222,10 +222,10 @@ def yaml_item(text): def create_items(self) -> None: items = [] - starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ") self.multiworld.push_precollected(self.create_item(starting_weapon)) self.multiworld.push_precollected(self.create_item("Steel Armor")) - if self.multiworld.sky_coin_mode[self.player] == "start_with": + if self.options.sky_coin_mode == "start_with": self.multiworld.push_precollected(self.create_item("Sky Coin")) precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} @@ -233,28 +233,28 @@ def create_items(self) -> None: def add_item(item_name): if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: return - if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key: return - if self.multiworld.progressive_gear[self.player]: + if self.options.progressive_gear: for item_group in prog_map: if item_name in self.item_name_groups[item_group]: item_name = prog_map[item_group] break if item_name == "Sky Coin": - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": for _ in range(40): items.append(self.create_item("Sky Fragment")) return - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": items.append(self.create_filler()) return if item_name in precollected_item_names: items.append(self.create_filler()) return i = self.create_item(item_name) - if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"): i.classification = ItemClassification.useful - if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + if (self.options.logic == "expert" and self.options.map_shuffle == "none" and item_name == "Exit Book"): i.classification = ItemClassification.progression items.append(i) @@ -263,11 +263,11 @@ def add_item(item_name): for item in self.item_name_groups[item_group]: add_item(item) - if self.multiworld.brown_boxes[self.player] == "include": + if self.options.brown_boxes == "include": filler_items = [] for item, count in fillers.items(): filler_items += [self.create_item(item) for _ in range(count)] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": self.multiworld.random.shuffle(filler_items) filler_items = filler_items[39:] items += filler_items diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index af3625f28a9d..41c397315f87 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,5 @@ -from Options import Choice, FreeText, Toggle, Range +from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class Logic(Choice): @@ -321,36 +322,36 @@ class KaelisMomFightsMinotaur(Toggle): default = 0 -option_definitions = { - "logic": Logic, - "brown_boxes": BrownBoxes, - "sky_coin_mode": SkyCoinMode, - "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, - "starting_weapon": StartingWeapon, - "progressive_gear": ProgressiveGear, - "leveling_curve": LevelingCurve, - "starting_companion": StartingCompanion, - "available_companions": AvailableCompanions, - "companions_locations": CompanionsLocations, - "kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur, - "companion_leveling_type": CompanionLevelingType, - "companion_spellbook_type": CompanionSpellbookType, - "enemies_density": EnemiesDensity, - "enemies_scaling_lower": EnemiesScalingLower, - "enemies_scaling_upper": EnemiesScalingUpper, - "bosses_scaling_lower": BossesScalingLower, - "bosses_scaling_upper": BossesScalingUpper, - "enemizer_attacks": EnemizerAttacks, - "enemizer_groups": EnemizerGroups, - "shuffle_res_weak_types": ShuffleResWeakType, - "shuffle_enemies_position": ShuffleEnemiesPositions, - "progressive_formations": ProgressiveFormations, - "doom_castle_mode": DoomCastle, - "doom_castle_shortcut": DoomCastleShortcut, - "tweak_frustrating_dungeons": TweakFrustratingDungeons, - "map_shuffle": MapShuffle, - "crest_shuffle": CrestShuffle, - "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, - "map_shuffle_seed": MapShuffleSeed, - "battlefields_battles_quantities": BattlefieldsBattlesQuantities, -} +@dataclass +class FFMQOptions(PerGameCommonOptions): + logic: Logic + brown_boxes: BrownBoxes + sky_coin_mode: SkyCoinMode + shattered_sky_coin_quantity: ShatteredSkyCoinQuantity + starting_weapon: StartingWeapon + progressive_gear: ProgressiveGear + leveling_curve: LevelingCurve + starting_companion: StartingCompanion + available_companions: AvailableCompanions + companions_locations: CompanionsLocations + kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur + companion_leveling_type: CompanionLevelingType + companion_spellbook_type: CompanionSpellbookType + enemies_density: EnemiesDensity + enemies_scaling_lower: EnemiesScalingLower + enemies_scaling_upper: EnemiesScalingUpper + bosses_scaling_lower: BossesScalingLower + bosses_scaling_upper: BossesScalingUpper + enemizer_attacks: EnemizerAttacks + enemizer_groups: EnemizerGroups + shuffle_res_weak_types: ShuffleResWeakType + shuffle_enemies_position: ShuffleEnemiesPositions + progressive_formations: ProgressiveFormations + doom_castle_mode: DoomCastle + doom_castle_shortcut: DoomCastleShortcut + tweak_frustrating_dungeons: TweakFrustratingDungeons + map_shuffle: MapShuffle + crest_shuffle: CrestShuffle + shuffle_battlefield_rewards: ShuffleBattlefieldRewards + map_shuffle_seed: MapShuffleSeed + battlefields_battles_quantities: BattlefieldsBattlesQuantities diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index 1b17aaa98f28..1e436a90c5fd 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -1,13 +1,13 @@ import yaml import os import zipfile +import Utils from copy import deepcopy from .Regions import object_id_table -from Utils import __version__ from worlds.Files import APPatch import pkgutil -settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) +settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml")) def generate_output(self, output_directory): @@ -21,7 +21,7 @@ def output_item_name(item): item_name = "".join(item_name.split(" ")) else: if item.advancement or item.useful or (item.trap and - self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + self.random.randint(0, 1)): item_name = "APItem" else: item_name = "APItemFiller" @@ -46,60 +46,60 @@ def tf(option): options = deepcopy(settings_template) options["name"] = self.multiworld.player_name[self.player] option_writes = { - "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "enemies_density": cc(self.options.enemies_density), "chests_shuffle": "Include", - "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "shuffle_boxes_content": self.options.brown_boxes == "shuffle", "npcs_shuffle": "Include", "battlefields_shuffle": "Include", - "logic_options": cc(self.multiworld.logic[self.player]), - "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), - "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), - "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), - "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), - "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), - "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), - "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), - "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if - self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "logic_options": cc(self.options.logic), + "shuffle_enemies_position": tf(self.options.shuffle_enemies_position), + "enemies_scaling_lower": cc(self.options.enemies_scaling_lower), + "enemies_scaling_upper": cc(self.options.enemies_scaling_upper), + "bosses_scaling_lower": cc(self.options.bosses_scaling_lower), + "bosses_scaling_upper": cc(self.options.bosses_scaling_upper), + "enemizer_attacks": cc(self.options.enemizer_attacks), + "leveling_curve": cc(self.options.leveling_curve), + "battles_quantity": cc(self.options.battlefields_battles_quantities) if + self.options.battlefields_battles_quantities.value < 5 else "RandomLow" if - self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + self.options.battlefields_battles_quantities.value == 5 else "RandomHigh", - "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards), "random_starting_weapon": True, - "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), - "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), - "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), - "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), - "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), - "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "progressive_gear": tf(self.options.progressive_gear), + "tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons), + "doom_castle_mode": cc(self.options.doom_castle_mode), + "doom_castle_shortcut": tf(self.options.doom_castle_shortcut), + "sky_coin_mode": cc(self.options.sky_coin_mode), + "sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity), "enable_spoilers": False, - "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), - "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), - "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), - "enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]), - "shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]), - "companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]), - "companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]), - "starting_companion": cc(self.multiworld.starting_companion[self.player]), + "progressive_formations": cc(self.options.progressive_formations), + "map_shuffling": cc(self.options.map_shuffle), + "crest_shuffle": tf(self.options.crest_shuffle), + "enemizer_groups": cc(self.options.enemizer_groups), + "shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types), + "companion_leveling_type": cc(self.options.companion_leveling_type), + "companion_spellbook_type": cc(self.options.companion_spellbook_type), + "starting_companion": cc(self.options.starting_companion), "available_companions": ["Zero", "One", "Two", - "Three", "Four"][self.multiworld.available_companions[self.player].value], - "companions_locations": cc(self.multiworld.companions_locations[self.player]), - "kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]), + "Three", "Four"][self.options.available_companions.value], + "companions_locations": cc(self.options.companions_locations), + "kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur), } for option, data in option_writes.items(): options["Final Fantasy Mystic Quest"][option][data] = 1 - rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] self.rom_name = bytearray(rom_name, 'utf8') self.rom_name_available_event.set() setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": - hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": starting_items.append("SkyCoin") file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index 8b83c88e72c9..f7b9b9eed4d8 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -1,11 +1,9 @@ from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule +from .data.rooms import rooms, entrances from .Items import item_groups, yaml_item -import pkgutil -import yaml -rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) -entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} +entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances} object_id_table = {} object_type_table = {} @@ -69,7 +67,7 @@ def create_regions(self): location_table else None, object["type"], object["access"], self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", - "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + "BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) @@ -91,15 +89,13 @@ def create_regions(self): if "entrance" in link and link["entrance"] != -1: spoiler = False if link["entrance"] in crest_warps: - if self.multiworld.crest_shuffle[self.player]: + if self.options.crest_shuffle: spoiler = True - elif self.multiworld.map_shuffle[self.player] == "everything": + elif self.options.map_shuffle == "everything": spoiler = True - elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", - "none"): + elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"): spoiler = True - elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", - "overworld"): + elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"): spoiler = True if spoiler: @@ -111,6 +107,7 @@ def create_regions(self): connection.connect(connect_room) break + non_dead_end_crest_rooms = [ 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', @@ -140,7 +137,7 @@ def hard_boss_logic(state): add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): loc = self.multiworld.get_location(boss, self.player) checked_regions = {loc.parent_region} @@ -158,12 +155,12 @@ def check_foresta(region): return True check_foresta(loc.parent_region) - if self.multiworld.logic[self.player] == "friendly": + if self.options.logic == "friendly": process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), ["MagicMirror"]) process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), ["Mask"]) - if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + if self.options.map_shuffle in ("none", "overworld"): process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), ["Bomb"]) process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), @@ -185,8 +182,8 @@ def check_foresta(region): process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), ["DragonClaw", "CaptainCap"]) - if self.multiworld.logic[self.player] == "expert": - if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + if self.options.logic == "expert": + if self.options.map_shuffle == "none" and not self.options.crest_shuffle: inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) @@ -198,14 +195,14 @@ def check_foresta(region): if entrance.connected_region.name in non_dead_end_crest_rooms: entrance.access_rule = lambda state: False - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + if self.options.sky_coin_mode == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value] self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Fragment", self.player, logic_coins) - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) - elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + elif self.options.sky_coin_mode in ("standard", "start_with"): self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Coin", self.player) @@ -213,26 +210,24 @@ def check_foresta(region): def stage_set_rules(multiworld): # If there's no enemies, there's no repeatable income sources no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") - if multiworld.enemies_density[player] == "none"] + if multiworld.worlds[player].options.enemies_density == "none"] if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, ItemClassification.trap)]) > len([player for player in no_enemies_players if - multiworld.accessibility[player] == "minimal"]) * 3): + multiworld.worlds[player].options.accessibility == "minimal"]) * 3): for player in no_enemies_players: for location in vendor_locations: - if multiworld.accessibility[player] == "locations": + if multiworld.worlds[player].options.accessibility == "locations": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False else: # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing - # advancement items so that useful items can be placed + # advancement items so that useful items can be placed. for player in no_enemies_players: for location in vendor_locations: multiworld.get_location(location, player).item_rule = lambda item: not item.advancement - - class FFMQLocation(Location): game = "Final Fantasy Mystic Quest" diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index ac3e91370933..c464203dc6a4 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -10,7 +10,7 @@ non_dead_end_crest_warps from .Items import item_table, item_groups, create_items, FFMQItem, fillers from .Output import generate_output -from .Options import option_definitions +from .Options import FFMQOptions from .Client import FFMQClient @@ -45,7 +45,8 @@ class FFMQWorld(World): item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} location_name_to_id = location_table - option_definitions = option_definitions + options_dataclass = FFMQOptions + options: FFMQOptions topology_present = True @@ -67,20 +68,14 @@ def __init__(self, world, player: int): super().__init__(world, player) def generate_early(self): - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - self.multiworld.brown_boxes[self.player].value = 1 - if self.multiworld.enemies_scaling_lower[self.player].value > \ - self.multiworld.enemies_scaling_upper[self.player].value: - (self.multiworld.enemies_scaling_lower[self.player].value, - self.multiworld.enemies_scaling_upper[self.player].value) =\ - (self.multiworld.enemies_scaling_upper[self.player].value, - self.multiworld.enemies_scaling_lower[self.player].value) - if self.multiworld.bosses_scaling_lower[self.player].value > \ - self.multiworld.bosses_scaling_upper[self.player].value: - (self.multiworld.bosses_scaling_lower[self.player].value, - self.multiworld.bosses_scaling_upper[self.player].value) =\ - (self.multiworld.bosses_scaling_upper[self.player].value, - self.multiworld.bosses_scaling_lower[self.player].value) + if self.options.sky_coin_mode == "shattered_sky_coin": + self.options.brown_boxes.value = 1 + if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value: + self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \ + self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value + if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value: + self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \ + self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value @classmethod def stage_generate_early(cls, multiworld): @@ -94,20 +89,20 @@ def stage_generate_early(cls, multiworld): rooms_data = {} for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): - if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or - world.multiworld.crest_shuffle[world.player]): - if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): - multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) - elif world.multiworld.map_shuffle_seed[world.player].value != "random": - multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) - + int(world.multiworld.seed)) + if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards + or world.options.companions_locations): + if world.options.map_shuffle_seed.value.isdigit(): + multiworld.random.seed(int(world.options.map_shuffle_seed.value)) + elif world.options.map_shuffle_seed.value != "random": + multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value)) + + int(world.multiworld.seed)) seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() - map_shuffle = multiworld.map_shuffle[world.player].value - crest_shuffle = multiworld.crest_shuffle[world.player].current_key - battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key - companion_shuffle = multiworld.companions_locations[world.player].value - kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key + map_shuffle = world.options.map_shuffle.value + crest_shuffle = world.options.crest_shuffle.current_key + battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key + companion_shuffle = world.options.companions_locations.value + kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" @@ -175,14 +170,14 @@ def get_filler_item_name(self): def extend_hint_information(self, hint_data): hint_data[self.player] = {} - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", "Subregion Doom Castle"]: region = self.multiworld.get_region(subregion, self.player) for location in region.locations: - if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + if location.address and self.options.map_shuffle != "dungeons": hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else "")) @@ -202,14 +197,13 @@ def extend_hint_information(self, hint_data): for location in exit_check.connected_region.locations: if location.address: hint = [] - if self.multiworld.map_shuffle[self.player] != "dungeons": + if self.options.map_shuffle != "dungeons": hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else ""))) - if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ - ("Subregion Mac's Ship", "Subregion Doom Castle"): + if self.options.map_shuffle != "overworld": hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", "Pazuzu's")) - hint = " - ".join(hint) + hint = " - ".join(hint).replace(" - Mac Ship", "") if location.address in hint_data[self.player]: hint_data[self.player][location.address] += f"/{hint}" else: diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml deleted file mode 100644 index 1dfef2655c37..000000000000 --- a/worlds/ffmq/data/entrances.yaml +++ /dev/null @@ -1,2450 +0,0 @@ -- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor - id: 0 - area: 7 - coordinates: [24, 19] - teleporter: [0, 0] -- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor - id: 1 - area: 7 - coordinates: [19, 43] - teleporter: [1, 6] -- name: Doom Castle - Aero Room - Aero Room Entrance - id: 2 - area: 7 - coordinates: [27, 39] - teleporter: [1, 0] -- name: Focus Tower B1 - Main Loop - South Entrance - id: 3 - area: 8 - coordinates: [43, 60] - teleporter: [2, 6] -- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall - id: 4 - area: 8 - coordinates: [37, 41] - teleporter: [4, 0] -- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room - id: 5 - area: 8 - coordinates: [59, 35] - teleporter: [5, 0] -- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest - id: 6 - area: 8 - coordinates: [57, 59] - teleporter: [8, 0] -- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door - id: 7 - area: 8 - coordinates: [51, 49] - teleporter: [6, 0] -- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor - id: 8 - area: 8 - coordinates: [51, 45] - teleporter: [7, 0] -- name: Focus Tower 1F - Focus Tower West Entrance - id: 9 - area: 9 - coordinates: [25, 29] - teleporter: [3, 6] -- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin - id: 10 - area: 9 - coordinates: [16, 4] - teleporter: [10, 0] -- name: Focus Tower 1F - To Focus Tower B1 - Main Hall - id: 11 - area: 9 - coordinates: [4, 23] - teleporter: [11, 0] -- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest - id: 12 - area: 9 - coordinates: [26, 17] - teleporter: [12, 0] -- name: Focus Tower 1F - Sky Door - id: 13 - area: 9 - coordinates: [16, 24] - teleporter: [13, 0] -- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin - id: 14 - area: 9 - coordinates: [16, 10] - teleporter: [14, 0] -- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door - id: 15 - area: 9 - coordinates: [16, 29] - teleporter: [15, 0] -- name: Focus Tower 2F - Sand Coin Passage - North Entrance - id: 16 - area: 10 - coordinates: [49, 30] - teleporter: [4, 6] -- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin - id: 17 - area: 10 - coordinates: [47, 33] - teleporter: [17, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin - id: 18 - area: 10 - coordinates: [47, 41] - teleporter: [18, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor - id: 19 - area: 10 - coordinates: [38, 40] - teleporter: [20, 0] -- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor - id: 20 - area: 10 - coordinates: [56, 40] - teleporter: [19, 0] -- name: Focus Tower 2F - Venus Chest Room - Pillar Script - id: 21 - area: 10 - coordinates: [48, 53] - teleporter: [13, 8] -- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance - id: 22 - area: 11 - coordinates: [11, 39] - teleporter: [6, 6] -- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar - id: 23 - area: 11 - coordinates: [6, 47] - teleporter: [24, 0] -- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance - id: 24 - area: 11 - coordinates: [21, 38] - teleporter: [5, 6] -- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room - id: 25 - area: 11 - coordinates: [24, 47] - teleporter: [23, 0] -- name: Level Forest - Boulder Script - id: 26 - area: 14 - coordinates: [52, 15] - teleporter: [0, 8] -- name: Level Forest - Rotten Tree Script - id: 27 - area: 14 - coordinates: [47, 6] - teleporter: [2, 8] -- name: Level Forest - Exit Level Forest 1 - id: 28 - area: 14 - coordinates: [46, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 2 - id: 29 - area: 14 - coordinates: [46, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 3 - id: 30 - area: 14 - coordinates: [47, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 4 - id: 31 - area: 14 - coordinates: [47, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 5 - id: 32 - area: 14 - coordinates: [60, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 6 - id: 33 - area: 14 - coordinates: [61, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 7 - id: 34 - area: 14 - coordinates: [46, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 8 - id: 35 - area: 14 - coordinates: [46, 3] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 9 - id: 36 - area: 14 - coordinates: [47, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest A - id: 37 - area: 14 - coordinates: [47, 3] - teleporter: [25, 0] -- name: Foresta - Exit Foresta 1 - id: 38 - area: 15 - coordinates: [10, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 2 - id: 39 - area: 15 - coordinates: [10, 26] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 3 - id: 40 - area: 15 - coordinates: [11, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 4 - id: 41 - area: 15 - coordinates: [11, 26] - teleporter: [31, 0] -- name: Foresta - Old Man House - Front Door - id: 42 - area: 15 - coordinates: [25, 17] - teleporter: [32, 4] -- name: Foresta - Old Man House - Back Door - id: 43 - area: 15 - coordinates: [25, 14] - teleporter: [33, 0] -- name: Foresta - Kaeli's House - id: 44 - area: 15 - coordinates: [7, 21] - teleporter: [0, 5] -- name: Foresta - Rest House - id: 45 - area: 15 - coordinates: [23, 23] - teleporter: [1, 5] -- name: Kaeli's House - Kaeli's House Entrance - id: 46 - area: 16 - coordinates: [11, 20] - teleporter: [86, 3] -- name: Foresta Houses - Old Man's House - Old Man Front Exit - id: 47 - area: 17 - coordinates: [35, 44] - teleporter: [34, 0] -- name: Foresta Houses - Old Man's House - Old Man Back Exit - id: 48 - area: 17 - coordinates: [35, 27] - teleporter: [35, 0] -- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script - id: 483 - area: 17 - coordinates: [0x23, 0x1E] - teleporter: [0x0D, 8] -- name: Foresta Houses - Rest House - Bed Script - id: 49 - area: 17 - coordinates: [30, 6] - teleporter: [1, 8] -- name: Foresta Houses - Rest House - Rest House Exit - id: 50 - area: 17 - coordinates: [35, 20] - teleporter: [87, 3] -- name: Foresta Houses - Libra House - Libra House Script - id: 51 - area: 17 - coordinates: [8, 49] - teleporter: [67, 8] -- name: Foresta Houses - Gemini House - Gemini House Script - id: 52 - area: 17 - coordinates: [26, 55] - teleporter: [68, 8] -- name: Foresta Houses - Mobius House - Mobius House Script - id: 53 - area: 17 - coordinates: [14, 33] - teleporter: [69, 8] -- name: Sand Temple - Sand Temple Entrance - id: 54 - area: 18 - coordinates: [56, 27] - teleporter: [36, 0] -- name: Bone Dungeon 1F - Bone Dungeon Entrance - id: 55 - area: 19 - coordinates: [13, 60] - teleporter: [37, 0] -- name: Bone Dungeon 1F - To Bone Dungeon B1 - id: 56 - area: 19 - coordinates: [13, 39] - teleporter: [2, 2] -- name: Bone Dungeon B1 - Waterway - Exit Waterway - id: 57 - area: 20 - coordinates: [27, 39] - teleporter: [3, 2] -- name: Bone Dungeon B1 - Waterway - Tristam's Script - id: 58 - area: 20 - coordinates: [27, 45] - teleporter: [3, 8] -- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F - id: 59 - area: 20 - coordinates: [54, 61] - teleporter: [88, 3] -- name: Bone Dungeon B1 - Checker Room - Exit Checker Room - id: 60 - area: 20 - coordinates: [23, 40] - teleporter: [4, 2] -- name: Bone Dungeon B1 - Checker Room - To Waterway - id: 61 - area: 20 - coordinates: [39, 49] - teleporter: [89, 3] -- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room - id: 62 - area: 20 - coordinates: [5, 33] - teleporter: [91, 3] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage - id: 63 - area: 21 - coordinates: [19, 13] - teleporter: [5, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room - id: 64 - area: 21 - coordinates: [29, 15] - teleporter: [6, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room - id: 65 - area: 21 - coordinates: [8, 25] - teleporter: [90, 3] -- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room - id: 66 - area: 21 - coordinates: [59, 12] - teleporter: [93, 3] -- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room - id: 67 - area: 21 - coordinates: [59, 28] - teleporter: [94, 3] -- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room - id: 68 - area: 21 - coordinates: [53, 7] - teleporter: [7, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room - id: 69 - area: 21 - coordinates: [41, 3] - teleporter: [8, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room - id: 70 - area: 21 - coordinates: [47, 57] - teleporter: [9, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room - id: 71 - area: 21 - coordinates: [54, 23] - teleporter: [92, 3] -- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script - id: 72 - area: 22 - coordinates: [29, 19] - teleporter: [4, 8] -- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script - id: 73 - area: 22 - coordinates: [29, 23] - teleporter: [75, 8] -- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room - id: 74 - area: 22 - coordinates: [30, 27] - teleporter: [95, 3] -- name: Libra Temple - Entrance - id: 75 - area: 23 - coordinates: [10, 15] - teleporter: [13, 6] -- name: Libra Temple - Libra Tile Script - id: 76 - area: 23 - coordinates: [9, 8] - teleporter: [59, 8] -- name: Aquaria Winter - Winter Entrance 1 - id: 77 - area: 24 - coordinates: [25, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 2 - id: 78 - area: 24 - coordinates: [25, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 3 - id: 79 - area: 24 - coordinates: [26, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 4 - id: 80 - area: 24 - coordinates: [26, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script - id: 81 - area: 24 - coordinates: [8, 19] - teleporter: [10, 5] # original value [5, 8] -- name: Aquaria Winter - Winter Vendor House Entrance - id: 82 - area: 24 - coordinates: [8, 5] - teleporter: [44, 4] -- name: Aquaria Winter - Winter INN Entrance - id: 83 - area: 24 - coordinates: [26, 17] - teleporter: [11, 5] -- name: Aquaria Summer - Summer Entrance 1 - id: 84 - area: 25 - coordinates: [57, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 2 - id: 85 - area: 25 - coordinates: [57, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 3 - id: 86 - area: 25 - coordinates: [58, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 4 - id: 87 - area: 25 - coordinates: [58, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Phoebe's House Entrance - id: 88 - area: 25 - coordinates: [40, 19] - teleporter: [10, 5] -- name: Aquaria Summer - Spencer's Place Entrance Top - id: 89 - area: 25 - coordinates: [40, 16] - teleporter: [42, 0] -- name: Aquaria Summer - Spencer's Place Entrance Side - id: 90 - area: 25 - coordinates: [41, 18] - teleporter: [43, 0] -- name: Aquaria Summer - Summer Vendor House Entrance - id: 91 - area: 25 - coordinates: [40, 5] - teleporter: [44, 4] -- name: Aquaria Summer - Summer INN Entrance - id: 92 - area: 25 - coordinates: [58, 17] - teleporter: [11, 5] -- name: Phoebe's House - Entrance # Change to a script, same as vendor house - id: 93 - area: 26 - coordinates: [29, 14] - teleporter: [5, 8] # Original Value [11,3] -- name: Aquaria Vendor House - Vendor House Entrance's Script - id: 94 - area: 27 - coordinates: [7, 10] - teleporter: [40, 8] -- name: Aquaria Vendor House - Vendor House Stairs - id: 95 - area: 27 - coordinates: [1, 4] - teleporter: [47, 0] -- name: Aquaria Gemini Room - Gemini Script - id: 96 - area: 27 - coordinates: [2, 40] - teleporter: [72, 8] -- name: Aquaria Gemini Room - Gemini Room Stairs - id: 97 - area: 27 - coordinates: [4, 39] - teleporter: [48, 0] -- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house - id: 98 - area: 27 - coordinates: [51, 46] - teleporter: [75, 8] # Original value [48,3] -- name: Wintry Cave 1F - Main Entrance - id: 99 - area: 28 - coordinates: [50, 58] - teleporter: [49, 0] -- name: Wintry Cave 1F - To 3F Top - id: 100 - area: 28 - coordinates: [40, 25] - teleporter: [14, 2] -- name: Wintry Cave 1F - To 2F - id: 101 - area: 28 - coordinates: [10, 43] - teleporter: [15, 2] -- name: Wintry Cave 1F - Phoebe's Script - id: 102 - area: 28 - coordinates: [44, 37] - teleporter: [6, 8] -- name: Wintry Cave 2F - To 3F Bottom - id: 103 - area: 29 - coordinates: [58, 5] - teleporter: [50, 0] -- name: Wintry Cave 2F - To 1F - id: 104 - area: 29 - coordinates: [38, 18] - teleporter: [97, 3] -- name: Wintry Cave 3F Top - Exit from 3F Top - id: 105 - area: 30 - coordinates: [24, 6] - teleporter: [96, 3] -- name: Wintry Cave 3F Bottom - Exit to 2F - id: 106 - area: 31 - coordinates: [4, 29] - teleporter: [51, 0] -- name: Life Temple - Entrance - id: 107 - area: 32 - coordinates: [9, 60] - teleporter: [14, 6] -- name: Life Temple - Libra Tile Script - id: 108 - area: 32 - coordinates: [3, 55] - teleporter: [60, 8] -- name: Life Temple - Mysterious Man Script - id: 109 - area: 32 - coordinates: [9, 44] - teleporter: [78, 8] -- name: Fall Basin - Back Exit Script - id: 110 - area: 33 - coordinates: [17, 5] - teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) -- name: Fall Basin - Main Exit - id: 111 - area: 33 - coordinates: [15, 26] - teleporter: [53, 0] -- name: Fall Basin - Phoebe's Script - id: 112 - area: 33 - coordinates: [17, 6] - teleporter: [9, 8] -- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room - id: 113 - area: 34 - coordinates: [43, 6] - teleporter: [55, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 1 - id: 114 - area: 35 - coordinates: [18, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 2 - id: 115 - area: 35 - coordinates: [19, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room - id: 116 - area: 35 - coordinates: [3, 27] - teleporter: [57, 0] -- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room - id: 117 - area: 35 - coordinates: [11, 15] - teleporter: [58, 0] -- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room - id: 118 - area: 35 - coordinates: [25, 16] - teleporter: [59, 0] -- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room - id: 119 - area: 35 - coordinates: [31, 1] - teleporter: [60, 0] -- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor - id: 120 - area: 35 - coordinates: [34, 9] - teleporter: [61, 0] -- name: Ice Pyramid 1F Maze - Statue's Script - id: 121 - area: 35 - coordinates: [21, 32] - teleporter: [77, 8] -- name: Ice Pyramid 2F South Tiled Room - To 1F - id: 122 - area: 36 - coordinates: [4, 26] - teleporter: [62, 0] -- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room - id: 123 - area: 36 - coordinates: [22, 17] - teleporter: [67, 0] -- name: Ice Pyramid 2F West Room - To 1F - id: 124 - area: 36 - coordinates: [9, 10] - teleporter: [63, 0] -- name: Ice Pyramid 2F Center Room - To 1F - id: 125 - area: 36 - coordinates: [22, 14] - teleporter: [64, 0] -- name: Ice Pyramid 2F Small North Room - To 1F - id: 126 - area: 36 - coordinates: [26, 4] - teleporter: [65, 0] -- name: Ice Pyramid 2F North Corridor - To 1F - id: 127 - area: 36 - coordinates: [32, 8] - teleporter: [66, 0] -- name: Ice Pyramid 2F North Corridor - To 3F Main Loop - id: 128 - area: 36 - coordinates: [12, 7] - teleporter: [68, 0] -- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room - id: 129 - area: 37 - coordinates: [24, 54] - teleporter: [69, 0] -- name: Ice Pyramid 3F Main Loop - To 2F Corridor - id: 130 - area: 37 - coordinates: [16, 45] - teleporter: [70, 0] -- name: Ice Pyramid 3F Main Loop - To 4F - id: 131 - area: 37 - coordinates: [19, 43] - teleporter: [71, 0] -- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop - id: 132 - area: 38 - coordinates: [52, 5] - teleporter: [72, 0] -- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room - id: 133 - area: 38 - coordinates: [62, 19] - teleporter: [73, 0] -- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room - id: 134 - area: 39 - coordinates: [54, 63] - teleporter: [74, 0] -- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate - id: 135 - area: 39 - coordinates: [47, 54] - teleporter: [77, 8] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room - id: 136 - area: 39 - coordinates: [39, 43] - teleporter: [75, 0] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room - id: 137 - area: 39 - coordinates: [39, 60] - teleporter: [76, 0] -- name: Ice Pyramid - Duplicate Ice Golem Room # not used? - id: 138 - area: 40 - coordinates: [44, 43] - teleporter: [77, 0] -- name: Ice Pyramid Climbing Wall Room - To Taunt Room - id: 139 - area: 41 - coordinates: [4, 59] - teleporter: [78, 0] -- name: Ice Pyramid Climbing Wall Room - To 5F Stairs - id: 140 - area: 41 - coordinates: [4, 45] - teleporter: [79, 0] -- name: Ice Pyramid Ice Golem Room - To 5F Stairs - id: 141 - area: 42 - coordinates: [44, 43] - teleporter: [80, 0] -- name: Ice Pyramid Ice Golem Room - Ice Golem Script - id: 142 - area: 42 - coordinates: [53, 32] - teleporter: [10, 8] -- name: Spencer Waterfall - To Spencer Cave - id: 143 - area: 43 - coordinates: [48, 57] - teleporter: [81, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 1 - id: 144 - area: 43 - coordinates: [40, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 2 - id: 145 - area: 43 - coordinates: [40, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 3 - id: 146 - area: 43 - coordinates: [41, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 4 - id: 147 - area: 43 - coordinates: [41, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 1 - id: 148 - area: 43 - coordinates: [46, 8] - teleporter: [83, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 2 - id: 149 - area: 43 - coordinates: [47, 8] - teleporter: [83, 0] -- name: Spencer Cave Normal Main - To Waterfall - id: 150 - area: 44 - coordinates: [14, 39] - teleporter: [85, 0] -- name: Spencer Cave Normal From Overworld - Exit to Overworld - id: 151 - area: 44 - coordinates: [15, 57] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Exit to Overworld - id: 152 - area: 45 - coordinates: [40, 29] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Libra Teleporter Start Script - id: 153 - area: 45 - coordinates: [28, 21] - teleporter: [33, 8] -- name: Spencer Cave Unplug - Libra Teleporter End Script - id: 154 - area: 45 - coordinates: [46, 4] - teleporter: [34, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Chest Script - id: 155 - area: 45 - coordinates: [21, 9] - teleporter: [35, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Start Script - id: 156 - area: 45 - coordinates: [29, 28] - teleporter: [36, 8] -- name: Wintry Temple Outer Room - Main Entrance - id: 157 - area: 46 - coordinates: [8, 31] - teleporter: [15, 6] -- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple - id: 158 - area: 46 - coordinates: [9, 24] - teleporter: [62, 8] -- name: Fireburg - To Overworld - id: 159 - area: 47 - coordinates: [4, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 160 - area: 47 - coordinates: [5, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 161 - area: 47 - coordinates: [28, 15] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 162 - area: 47 - coordinates: [27, 15] - teleporter: [9, 6] -- name: Fireburg - Vendor House - id: 163 - area: 47 - coordinates: [10, 24] - teleporter: [91, 0] -- name: Fireburg - Reuben House - id: 164 - area: 47 - coordinates: [14, 6] - teleporter: [98, 8] # Script for reuben, original value [16, 2] -- name: Fireburg - Hotel - id: 165 - area: 47 - coordinates: [20, 8] - teleporter: [96, 8] # It's a script now for tristam, original value [17, 2] -- name: Fireburg - GrenadeMan House Script - id: 166 - area: 47 - coordinates: [12, 18] - teleporter: [11, 8] -- name: Reuben House - Main Entrance - id: 167 - area: 48 - coordinates: [33, 46] - teleporter: [98, 3] -- name: GrenadeMan House - Entrance Script - id: 168 - area: 49 - coordinates: [55, 60] - teleporter: [9, 8] -- name: GrenadeMan House - To Mobius Crest Room - id: 169 - area: 49 - coordinates: [57, 52] - teleporter: [93, 0] -- name: GrenadeMan Mobius Room - Stairs to House - id: 170 - area: 49 - coordinates: [39, 26] - teleporter: [94, 0] -- name: GrenadeMan Mobius Room - Mobius Teleporter Script - id: 171 - area: 49 - coordinates: [39, 23] - teleporter: [54, 8] -- name: Fireburg Vendor House - Entrance Script # No use to be a script - id: 172 - area: 49 - coordinates: [7, 10] - teleporter: [95, 0] # Original value [39, 8] -- name: Fireburg Vendor House - Stairs to Gemini Room - id: 173 - area: 49 - coordinates: [1, 4] - teleporter: [96, 0] -- name: Fireburg Gemini Room - Stairs to Vendor House - id: 174 - area: 49 - coordinates: [4, 39] - teleporter: [97, 0] -- name: Fireburg Gemini Room - Gemini Teleporter Script - id: 175 - area: 49 - coordinates: [2, 40] - teleporter: [45, 8] -- name: Fireburg Hotel Lobby - Stairs to beds - id: 176 - area: 49 - coordinates: [4, 50] - teleporter: [213, 0] -- name: Fireburg Hotel Lobby - Entrance - id: 177 - area: 49 - coordinates: [17, 56] - teleporter: [99, 3] -- name: Fireburg Hotel Beds - Stairs to Hotel Lobby - id: 178 - area: 49 - coordinates: [45, 59] - teleporter: [214, 0] -- name: Mine Exterior - Main Entrance - id: 179 - area: 50 - coordinates: [5, 28] - teleporter: [98, 0] -- name: Mine Exterior - To Cliff - id: 180 - area: 50 - coordinates: [58, 29] - teleporter: [99, 0] -- name: Mine Exterior - To Parallel Room - id: 181 - area: 50 - coordinates: [8, 7] - teleporter: [20, 2] -- name: Mine Exterior - To Crescent Room - id: 182 - area: 50 - coordinates: [26, 15] - teleporter: [21, 2] -- name: Mine Exterior - To Climbing Room - id: 183 - area: 50 - coordinates: [21, 35] - teleporter: [22, 2] -- name: Mine Exterior - Jinn Fight Script - id: 184 - area: 50 - coordinates: [58, 31] - teleporter: [74, 8] -- name: Mine Parallel Room - To Mine Exterior - id: 185 - area: 51 - coordinates: [7, 60] - teleporter: [100, 3] -- name: Mine Crescent Room - To Mine Exterior - id: 186 - area: 51 - coordinates: [22, 61] - teleporter: [101, 3] -- name: Mine Climbing Room - To Mine Exterior - id: 187 - area: 51 - coordinates: [56, 21] - teleporter: [102, 3] -- name: Mine Cliff - Entrance - id: 188 - area: 52 - coordinates: [9, 5] - teleporter: [100, 0] -- name: Mine Cliff - Reuben Grenade Script - id: 189 - area: 52 - coordinates: [15, 7] - teleporter: [12, 8] -- name: Sealed Temple - To Overworld - id: 190 - area: 53 - coordinates: [58, 43] - teleporter: [16, 6] -- name: Sealed Temple - Gemini Tile Script - id: 191 - area: 53 - coordinates: [56, 38] - teleporter: [63, 8] -- name: Volcano Base - Main Entrance 1 - id: 192 - area: 54 - coordinates: [23, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 2 - id: 193 - area: 54 - coordinates: [23, 26] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 3 - id: 194 - area: 54 - coordinates: [24, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 4 - id: 195 - area: 54 - coordinates: [24, 26] - teleporter: [103, 0] -- name: Volcano Base - Left Stairs Script - id: 196 - area: 54 - coordinates: [20, 5] - teleporter: [31, 8] -- name: Volcano Base - Right Stairs Script - id: 197 - area: 54 - coordinates: [32, 5] - teleporter: [30, 8] -- name: Volcano Top Right - Top Exit - id: 198 - area: 55 - coordinates: [44, 8] - teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up -- name: Volcano Top Left - To Right-Left Path Script - id: 199 - area: 55 - coordinates: [40, 24] - teleporter: [26, 8] -- name: Volcano Top Right - To Left-Right Path Script - id: 200 - area: 55 - coordinates: [52, 24] - teleporter: [79, 8] # Original Value [26, 8] -- name: Volcano Right Path - To Volcano Base Script - id: 201 - area: 56 - coordinates: [48, 42] - teleporter: [15, 8] # Original Value [27, 8] -- name: Volcano Left Path - To Volcano Cross Left-Right - id: 202 - area: 56 - coordinates: [40, 31] - teleporter: [25, 2] -- name: Volcano Left Path - To Volcano Cross Right-Left - id: 203 - area: 56 - coordinates: [52, 29] - teleporter: [26, 2] -- name: Volcano Left Path - To Volcano Base Script - id: 204 - area: 56 - coordinates: [36, 42] - teleporter: [27, 8] -- name: Volcano Cross Left-Right - To Volcano Left Path - id: 205 - area: 56 - coordinates: [10, 42] - teleporter: [103, 3] -- name: Volcano Cross Left-Right - To Volcano Top Right Script - id: 206 - area: 56 - coordinates: [16, 24] - teleporter: [29, 8] -- name: Volcano Cross Right-Left - To Volcano Top Left Script - id: 207 - area: 56 - coordinates: [8, 22] - teleporter: [28, 8] -- name: Volcano Cross Right-Left - To Volcano Left Path - id: 208 - area: 56 - coordinates: [16, 42] - teleporter: [104, 3] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 - id: 209 - area: 57 - coordinates: [32, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 - id: 210 - area: 57 - coordinates: [33, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - To Three Steps Room - id: 211 - area: 57 - coordinates: [14, 5] - teleporter: [105, 0] -- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower - id: 212 - area: 57 - coordinates: [40, 17] - teleporter: [106, 0] -- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left - id: 213 - area: 57 - coordinates: [8, 11] - teleporter: [108, 0] -- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room - id: 214 - area: 57 - coordinates: [11, 19] - teleporter: [111, 0] -- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher - id: 215 - area: 57 - coordinates: [32, 11] - teleporter: [107, 0] -- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor - id: 216 - area: 57 - coordinates: [12, 23] - teleporter: [109, 0] -- name: Lava Dome Inner Ring Plate Ledge - Plate Script - id: 217 - area: 57 - coordinates: [5, 23] - teleporter: [47, 8] -- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room - id: 218 - area: 57 - coordinates: [0, 9] - teleporter: [110, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room - id: 219 - area: 57 - coordinates: [0, 15] - teleporter: [112, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor - id: 220 - area: 57 - coordinates: [54, 5] - teleporter: [113, 0] -- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II - id: 221 - area: 57 - coordinates: [54, 21] - teleporter: [114, 0] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 - id: 222 - area: 57 - coordinates: [62, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 - id: 223 - area: 57 - coordinates: [63, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 - id: 224 - area: 57 - coordinates: [62, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 - id: 225 - area: 57 - coordinates: [63, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor - id: 226 - area: 57 - coordinates: [50, 25] - teleporter: [115, 0] -- name: Lava Dome Jump Maze II - Lower Right Entrance - id: 227 - area: 58 - coordinates: [55, 28] - teleporter: [116, 0] -- name: Lava Dome Jump Maze II - Upper Entrance - id: 228 - area: 58 - coordinates: [35, 3] - teleporter: [119, 0] -- name: Lava Dome Jump Maze II - Lower Left Entrance - id: 229 - area: 58 - coordinates: [34, 27] - teleporter: [120, 0] -- name: Lava Dome Up-Down Corridor - Upper Entrance - id: 230 - area: 58 - coordinates: [29, 8] - teleporter: [117, 0] -- name: Lava Dome Up-Down Corridor - Lower Entrance - id: 231 - area: 58 - coordinates: [28, 25] - teleporter: [118, 0] -- name: Lava Dome Jump Maze I - South Entrance - id: 232 - area: 59 - coordinates: [20, 27] - teleporter: [121, 0] -- name: Lava Dome Jump Maze I - North Entrance - id: 233 - area: 59 - coordinates: [7, 3] - teleporter: [122, 0] -- name: Lava Dome Pointless Room - Entrance - id: 234 - area: 60 - coordinates: [2, 7] - teleporter: [123, 0] -- name: Lava Dome Pointless Room - Visit Quest Script 1 - id: 490 - area: 60 - coordinates: [4, 4] - teleporter: [99, 8] -- name: Lava Dome Pointless Room - Visit Quest Script 2 - id: 491 - area: 60 - coordinates: [4, 5] - teleporter: [99, 8] -- name: Lava Dome Lower Moon Helm Room - Left Entrance - id: 235 - area: 60 - coordinates: [2, 19] - teleporter: [124, 0] -- name: Lava Dome Lower Moon Helm Room - Right Entrance - id: 236 - area: 60 - coordinates: [11, 21] - teleporter: [125, 0] -- name: Lava Dome Moon Helm Room - Entrance - id: 237 - area: 60 - coordinates: [15, 23] - teleporter: [126, 0] -- name: Lava Dome Three Jumps Room - To Main Loop - id: 238 - area: 61 - coordinates: [58, 15] - teleporter: [127, 0] -- name: Lava Dome Life Chest Room - Lower South Entrance - id: 239 - area: 61 - coordinates: [38, 27] - teleporter: [128, 0] -- name: Lava Dome Life Chest Room - Upper South Entrance - id: 240 - area: 61 - coordinates: [28, 23] - teleporter: [129, 0] -- name: Lava Dome Big Jump Room - Left Entrance - id: 241 - area: 62 - coordinates: [42, 51] - teleporter: [133, 0] -- name: Lava Dome Big Jump Room - North Entrance - id: 242 - area: 62 - coordinates: [30, 29] - teleporter: [131, 0] -- name: Lava Dome Big Jump Room - Lower Right Stairs - id: 243 - area: 62 - coordinates: [61, 59] - teleporter: [132, 0] -- name: Lava Dome Split Corridor - Upper Stairs - id: 244 - area: 62 - coordinates: [30, 43] - teleporter: [130, 0] -- name: Lava Dome Split Corridor - Lower Stairs - id: 245 - area: 62 - coordinates: [36, 61] - teleporter: [134, 0] -- name: Lava Dome Plate Corridor - Right Entrance - id: 246 - area: 63 - coordinates: [19, 29] - teleporter: [135, 0] -- name: Lava Dome Plate Corridor - Left Entrance - id: 247 - area: 63 - coordinates: [60, 21] - teleporter: [137, 0] -- name: Lava Dome Four Boxes Stairs - Upper Entrance - id: 248 - area: 63 - coordinates: [22, 3] - teleporter: [136, 0] -- name: Lava Dome Four Boxes Stairs - Lower Entrance - id: 249 - area: 63 - coordinates: [22, 17] - teleporter: [16, 0] -- name: Lava Dome Hydra Room - South Entrance - id: 250 - area: 64 - coordinates: [14, 59] - teleporter: [105, 3] -- name: Lava Dome Hydra Room - North Exit - id: 251 - area: 64 - coordinates: [25, 31] - teleporter: [138, 0] -- name: Lava Dome Hydra Room - Hydra Script - id: 252 - area: 64 - coordinates: [14, 36] - teleporter: [14, 8] -- name: Lava Dome Escape Corridor - South Entrance - id: 253 - area: 65 - coordinates: [22, 17] - teleporter: [139, 0] -- name: Lava Dome Escape Corridor - North Entrance - id: 254 - area: 65 - coordinates: [22, 3] - teleporter: [9, 0] -- name: Rope Bridge - West Entrance 1 - id: 255 - area: 66 - coordinates: [3, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 2 - id: 256 - area: 66 - coordinates: [3, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 3 - id: 257 - area: 66 - coordinates: [3, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 4 - id: 258 - area: 66 - coordinates: [3, 13] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 5 - id: 259 - area: 66 - coordinates: [4, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 6 - id: 260 - area: 66 - coordinates: [4, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 7 - id: 261 - area: 66 - coordinates: [4, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 8 - id: 262 - area: 66 - coordinates: [4, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 1 - id: 263 - area: 66 - coordinates: [59, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 2 - id: 264 - area: 66 - coordinates: [59, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 3 - id: 265 - area: 66 - coordinates: [59, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 4 - id: 266 - area: 66 - coordinates: [59, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 5 - id: 267 - area: 66 - coordinates: [60, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 6 - id: 268 - area: 66 - coordinates: [60, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 7 - id: 269 - area: 66 - coordinates: [60, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 8 - id: 270 - area: 66 - coordinates: [60, 13] - teleporter: [140, 0] -- name: Rope Bridge - Reuben Fall Script - id: 271 - area: 66 - coordinates: [13, 12] - teleporter: [15, 8] -- name: Alive Forest - West Entrance 1 - id: 272 - area: 67 - coordinates: [8, 13] - teleporter: [142, 0] -- name: Alive Forest - West Entrance 2 - id: 273 - area: 67 - coordinates: [9, 13] - teleporter: [142, 0] -- name: Alive Forest - Giant Tree Entrance - id: 274 - area: 67 - coordinates: [42, 42] - teleporter: [143, 0] -- name: Alive Forest - Libra Teleporter Script - id: 275 - area: 67 - coordinates: [8, 52] - teleporter: [64, 8] -- name: Alive Forest - Gemini Teleporter Script - id: 276 - area: 67 - coordinates: [57, 49] - teleporter: [65, 8] -- name: Alive Forest - Mobius Teleporter Script - id: 277 - area: 67 - coordinates: [24, 10] - teleporter: [66, 8] -- name: Giant Tree 1F - Entrance Script 1 - id: 278 - area: 68 - coordinates: [18, 31] - teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] -- name: Giant Tree 1F - Entrance Script 2 - id: 279 - area: 68 - coordinates: [19, 31] - teleporter: [56, 1] # Same [49, 8] -- name: Giant Tree 1F - North Entrance To 2F - id: 280 - area: 68 - coordinates: [16, 1] - teleporter: [144, 0] -- name: Giant Tree 2F Main Lobby - North Entrance to 1F - id: 281 - area: 69 - coordinates: [44, 33] - teleporter: [145, 0] -- name: Giant Tree 2F Main Lobby - Central Entrance to 3F - id: 282 - area: 69 - coordinates: [42, 47] - teleporter: [146, 0] -- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room - id: 283 - area: 69 - coordinates: [58, 49] - teleporter: [149, 0] -- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge - id: 284 - area: 69 - coordinates: [34, 37] - teleporter: [147, 0] -- name: Giant Tree 2F Fall From Vine Script - id: 482 - area: 69 - coordinates: [0x2E, 0x33] - teleporter: [76, 8] -- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room - id: 285 - area: 69 - coordinates: [58, 44] - teleporter: [148, 0] -- name: Giant Tree 2F Mushroom Room - Entrance - id: 286 - area: 70 - coordinates: [55, 18] - teleporter: [150, 0] -- name: Giant Tree 2F Mushroom Room - North Face to Meteor - id: 287 - area: 70 - coordinates: [56, 7] - teleporter: [151, 0] -- name: Giant Tree 3F Central Room - Central Entrance to 2F - id: 288 - area: 71 - coordinates: [46, 53] - teleporter: [152, 0] -- name: Giant Tree 3F Central Room - East Entrance to Worm Room - id: 289 - area: 71 - coordinates: [58, 39] - teleporter: [153, 0] -- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room - id: 290 - area: 71 - coordinates: [45, 39] - teleporter: [154, 0] -- name: Giant Tree 3F West Platform - Lower Entrance - id: 291 - area: 71 - coordinates: [33, 43] - teleporter: [155, 0] -- name: Giant Tree 3F West Platform - Top Entrance - id: 292 - area: 71 - coordinates: [52, 25] - teleporter: [156, 0] -- name: Giant Tree Worm Room - East Entrance - id: 293 - area: 72 - coordinates: [20, 58] - teleporter: [157, 0] -- name: Giant Tree Worm Room - West Entrance - id: 294 - area: 72 - coordinates: [6, 56] - teleporter: [158, 0] -- name: Giant Tree 4F Lower Floor - Entrance - id: 295 - area: 73 - coordinates: [20, 7] - teleporter: [159, 0] -- name: Giant Tree 4F Lower Floor - Lower West Mouth - id: 296 - area: 73 - coordinates: [8, 23] - teleporter: [160, 0] -- name: Giant Tree 4F Lower Floor - Lower Central Mouth - id: 297 - area: 73 - coordinates: [14, 25] - teleporter: [161, 0] -- name: Giant Tree 4F Lower Floor - Lower East Mouth - id: 298 - area: 73 - coordinates: [20, 25] - teleporter: [162, 0] -- name: Giant Tree 4F Upper Floor - Upper West Mouth - id: 299 - area: 73 - coordinates: [8, 19] - teleporter: [163, 0] -- name: Giant Tree 4F Upper Floor - Upper Central Mouth - id: 300 - area: 73 - coordinates: [12, 17] - teleporter: [164, 0] -- name: Giant Tree 4F Slime Room - Exit - id: 301 - area: 74 - coordinates: [47, 10] - teleporter: [165, 0] -- name: Giant Tree 4F Slime Room - West Entrance - id: 302 - area: 74 - coordinates: [45, 24] - teleporter: [166, 0] -- name: Giant Tree 4F Slime Room - Central Entrance - id: 303 - area: 74 - coordinates: [50, 24] - teleporter: [167, 0] -- name: Giant Tree 4F Slime Room - East Entrance - id: 304 - area: 74 - coordinates: [57, 28] - teleporter: [168, 0] -- name: Giant Tree 5F - Entrance - id: 305 - area: 75 - coordinates: [14, 51] - teleporter: [169, 0] -- name: Giant Tree 5F - Giant Tree Face # Unused - id: 306 - area: 75 - coordinates: [14, 37] - teleporter: [170, 0] -- name: Kaidge Temple - Entrance - id: 307 - area: 77 - coordinates: [44, 63] - teleporter: [18, 6] -- name: Kaidge Temple - Mobius Teleporter Script - id: 308 - area: 77 - coordinates: [35, 57] - teleporter: [71, 8] -- name: Windhole Temple - Entrance - id: 309 - area: 78 - coordinates: [10, 29] - teleporter: [173, 0] -- name: Mount Gale - Entrance 1 - id: 310 - area: 79 - coordinates: [1, 45] - teleporter: [174, 0] -- name: Mount Gale - Entrance 2 - id: 311 - area: 79 - coordinates: [2, 45] - teleporter: [174, 0] -- name: Mount Gale - Visit Quest - id: 494 - area: 79 - coordinates: [44, 7] - teleporter: [101, 8] -- name: Windia - Main Entrance 1 - id: 312 - area: 80 - coordinates: [12, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 2 - id: 313 - area: 80 - coordinates: [13, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 3 - id: 314 - area: 80 - coordinates: [14, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 4 - id: 315 - area: 80 - coordinates: [15, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 5 - id: 316 - area: 80 - coordinates: [12, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 6 - id: 317 - area: 80 - coordinates: [13, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 7 - id: 318 - area: 80 - coordinates: [14, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 8 - id: 319 - area: 80 - coordinates: [15, 41] - teleporter: [10, 6] -- name: Windia - Otto's House - id: 320 - area: 80 - coordinates: [21, 39] - teleporter: [30, 5] -- name: Windia - INN's Script # Change to teleporter / Change back to script! - id: 321 - area: 80 - coordinates: [18, 34] - teleporter: [97, 8] # Original value [79, 8] > [31, 2] -- name: Windia - Vendor House - id: 322 - area: 80 - coordinates: [8, 36] - teleporter: [32, 5] -- name: Windia - Kid House - id: 323 - area: 80 - coordinates: [7, 23] - teleporter: [176, 4] -- name: Windia - Old People House - id: 324 - area: 80 - coordinates: [19, 21] - teleporter: [177, 4] -- name: Windia - Rainbow Bridge Script - id: 325 - area: 80 - coordinates: [21, 9] - teleporter: [10, 6] # Change to entrance, usually a script [41, 8] -- name: Otto's House - Attic Stairs - id: 326 - area: 81 - coordinates: [2, 19] - teleporter: [33, 2] -- name: Otto's House - Entrance - id: 327 - area: 81 - coordinates: [9, 30] - teleporter: [106, 3] -- name: Otto's Attic - Stairs - id: 328 - area: 81 - coordinates: [26, 23] - teleporter: [107, 3] -- name: Windia Kid House - Entrance Script # Change to teleporter - id: 329 - area: 82 - coordinates: [7, 10] - teleporter: [178, 0] # Original value [38, 8] -- name: Windia Kid House - Basement Stairs - id: 330 - area: 82 - coordinates: [1, 4] - teleporter: [180, 0] -- name: Windia Old People House - Entrance - id: 331 - area: 82 - coordinates: [55, 12] - teleporter: [179, 0] -- name: Windia Old People House - Basement Stairs - id: 332 - area: 82 - coordinates: [60, 5] - teleporter: [181, 0] -- name: Windia Kid House Basement - Stairs - id: 333 - area: 82 - coordinates: [43, 8] - teleporter: [182, 0] -- name: Windia Kid House Basement - Mobius Teleporter - id: 334 - area: 82 - coordinates: [41, 9] - teleporter: [44, 8] -- name: Windia Old People House Basement - Stairs - id: 335 - area: 82 - coordinates: [39, 26] - teleporter: [183, 0] -- name: Windia Old People House Basement - Mobius Teleporter Script - id: 336 - area: 82 - coordinates: [39, 23] - teleporter: [43, 8] -- name: Windia Inn Lobby - Stairs to Beds - id: 337 - area: 82 - coordinates: [45, 24] - teleporter: [102, 8] # Changed to script, original value [215, 0] -- name: Windia Inn Lobby - Exit - id: 338 - area: 82 - coordinates: [53, 30] - teleporter: [135, 3] -- name: Windia Inn Beds - Stairs to Lobby - id: 339 - area: 82 - coordinates: [33, 59] - teleporter: [216, 0] -- name: Windia Vendor House - Entrance - id: 340 - area: 82 - coordinates: [29, 14] - teleporter: [108, 3] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 - id: 341 - area: 83 - coordinates: [47, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 - id: 342 - area: 83 - coordinates: [47, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 - id: 343 - area: 83 - coordinates: [48, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 - id: 344 - area: 83 - coordinates: [48, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - East Entrance - id: 345 - area: 83 - coordinates: [55, 12] - teleporter: [185, 0] -- name: Pazuzu Tower 1F Main Lobby - South Stairs - id: 346 - area: 83 - coordinates: [51, 25] - teleporter: [186, 0] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 - id: 347 - area: 83 - coordinates: [47, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 - id: 348 - area: 83 - coordinates: [48, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Boxes Room - West Stairs - id: 349 - area: 83 - coordinates: [38, 17] - teleporter: [187, 0] -- name: Pazuzu 2F - West Upper Stairs - id: 350 - area: 84 - coordinates: [7, 11] - teleporter: [188, 0] -- name: Pazuzu 2F - South Stairs - id: 351 - area: 84 - coordinates: [20, 24] - teleporter: [189, 0] -- name: Pazuzu 2F - West Lower Stairs - id: 352 - area: 84 - coordinates: [6, 17] - teleporter: [190, 0] -- name: Pazuzu 2F - Central Stairs - id: 353 - area: 84 - coordinates: [15, 15] - teleporter: [191, 0] -- name: Pazuzu 2F - Pazuzu 1 - id: 354 - area: 84 - coordinates: [15, 8] - teleporter: [17, 8] -- name: Pazuzu 2F - Pazuzu 2 - id: 355 - area: 84 - coordinates: [16, 8] - teleporter: [17, 8] -- name: Pazuzu 3F Main Room - North Stairs - id: 356 - area: 85 - coordinates: [23, 11] - teleporter: [192, 0] -- name: Pazuzu 3F Main Room - West Stairs - id: 357 - area: 85 - coordinates: [7, 15] - teleporter: [193, 0] -- name: Pazuzu 3F Main Room - Pazuzu Script 1 - id: 358 - area: 85 - coordinates: [15, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Main Room - Pazuzu Script 2 - id: 359 - area: 85 - coordinates: [16, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Central Island - Central Stairs - id: 360 - area: 85 - coordinates: [15, 14] - teleporter: [194, 0] -- name: Pazuzu 3F Central Island - South Stairs - id: 361 - area: 85 - coordinates: [17, 25] - teleporter: [195, 0] -- name: Pazuzu 4F - Northwest Stairs - id: 362 - area: 86 - coordinates: [39, 12] - teleporter: [196, 0] -- name: Pazuzu 4F - Southwest Stairs - id: 363 - area: 86 - coordinates: [39, 19] - teleporter: [197, 0] -- name: Pazuzu 4F - South Stairs - id: 364 - area: 86 - coordinates: [47, 24] - teleporter: [198, 0] -- name: Pazuzu 4F - Northeast Stairs - id: 365 - area: 86 - coordinates: [54, 9] - teleporter: [199, 0] -- name: Pazuzu 4F - Pazuzu Script 1 - id: 366 - area: 86 - coordinates: [47, 8] - teleporter: [19, 8] -- name: Pazuzu 4F - Pazuzu Script 2 - id: 367 - area: 86 - coordinates: [48, 8] - teleporter: [19, 8] -- name: Pazuzu 5F Pazuzu Loop - West Stairs - id: 368 - area: 87 - coordinates: [9, 49] - teleporter: [200, 0] -- name: Pazuzu 5F Pazuzu Loop - South Stairs - id: 369 - area: 87 - coordinates: [16, 55] - teleporter: [201, 0] -- name: Pazuzu 5F Upper Loop - Northeast Stairs - id: 370 - area: 87 - coordinates: [22, 40] - teleporter: [202, 0] -- name: Pazuzu 5F Upper Loop - Northwest Stairs - id: 371 - area: 87 - coordinates: [9, 40] - teleporter: [203, 0] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 - id: 372 - area: 87 - coordinates: [15, 40] - teleporter: [20, 8] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 - id: 373 - area: 87 - coordinates: [16, 40] - teleporter: [20, 8] -- name: Pazuzu 6F - West Stairs - id: 374 - area: 88 - coordinates: [41, 47] - teleporter: [204, 0] -- name: Pazuzu 6F - Northwest Stairs - id: 375 - area: 88 - coordinates: [41, 40] - teleporter: [205, 0] -- name: Pazuzu 6F - Northeast Stairs - id: 376 - area: 88 - coordinates: [54, 40] - teleporter: [206, 0] -- name: Pazuzu 6F - South Stairs - id: 377 - area: 88 - coordinates: [52, 56] - teleporter: [207, 0] -- name: Pazuzu 6F - Pazuzu Script 1 - id: 378 - area: 88 - coordinates: [47, 40] - teleporter: [21, 8] -- name: Pazuzu 6F - Pazuzu Script 2 - id: 379 - area: 88 - coordinates: [48, 40] - teleporter: [21, 8] -- name: Pazuzu 7F Main Room - Southwest Stairs - id: 380 - area: 89 - coordinates: [15, 54] - teleporter: [26, 0] -- name: Pazuzu 7F Main Room - Northeast Stairs - id: 381 - area: 89 - coordinates: [21, 40] - teleporter: [27, 0] -- name: Pazuzu 7F Main Room - Southeast Stairs - id: 382 - area: 89 - coordinates: [21, 56] - teleporter: [28, 0] -- name: Pazuzu 7F Main Room - Pazuzu Script 1 - id: 383 - area: 89 - coordinates: [15, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Pazuzu Script 2 - id: 384 - area: 89 - coordinates: [16, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle - id: 480 - area: 89 - coordinates: [15, 40] - teleporter: [38, 8] -- name: Pazuzu 1F to 3F - South Stairs - id: 385 - area: 90 - coordinates: [43, 60] - teleporter: [29, 0] -- name: Pazuzu 1F to 3F - North Stairs - id: 386 - area: 90 - coordinates: [43, 36] - teleporter: [30, 0] -- name: Pazuzu 3F to 5F - South Stairs - id: 387 - area: 91 - coordinates: [43, 60] - teleporter: [40, 0] -- name: Pazuzu 3F to 5F - North Stairs - id: 388 - area: 91 - coordinates: [43, 36] - teleporter: [41, 0] -- name: Pazuzu 5F to 7F - South Stairs - id: 389 - area: 92 - coordinates: [43, 60] - teleporter: [38, 0] -- name: Pazuzu 5F to 7F - North Stairs - id: 390 - area: 92 - coordinates: [43, 36] - teleporter: [39, 0] -- name: Pazuzu 2F to 4F - South Stairs - id: 391 - area: 93 - coordinates: [43, 60] - teleporter: [21, 0] -- name: Pazuzu 2F to 4F - North Stairs - id: 392 - area: 93 - coordinates: [43, 36] - teleporter: [22, 0] -- name: Pazuzu 4F to 6F - South Stairs - id: 393 - area: 94 - coordinates: [43, 60] - teleporter: [2, 0] -- name: Pazuzu 4F to 6F - North Stairs - id: 394 - area: 94 - coordinates: [43, 36] - teleporter: [3, 0] -- name: Light Temple - Entrance - id: 395 - area: 95 - coordinates: [28, 57] - teleporter: [19, 6] -- name: Light Temple - Mobius Teleporter Script - id: 396 - area: 95 - coordinates: [29, 37] - teleporter: [70, 8] -- name: Light Temple - Visit Quest Script 1 - id: 492 - area: 95 - coordinates: [34, 39] - teleporter: [100, 8] -- name: Light Temple - Visit Quest Script 2 - id: 493 - area: 95 - coordinates: [35, 39] - teleporter: [100, 8] -- name: Ship Dock - Mobius Teleporter Script - id: 397 - area: 96 - coordinates: [15, 18] - teleporter: [61, 8] -- name: Ship Dock - From Overworld - id: 398 - area: 96 - coordinates: [15, 11] - teleporter: [73, 0] -- name: Ship Dock - Entrance - id: 399 - area: 96 - coordinates: [15, 23] - teleporter: [17, 6] -- name: Mac Ship Deck - East Entrance Script - id: 400 - area: 97 - coordinates: [26, 40] - teleporter: [37, 8] -- name: Mac Ship Deck - Central Stairs Script - id: 401 - area: 97 - coordinates: [16, 47] - teleporter: [50, 8] -- name: Mac Ship Deck - West Stairs Script - id: 402 - area: 97 - coordinates: [8, 34] - teleporter: [51, 8] -- name: Mac Ship Deck - East Stairs Script - id: 403 - area: 97 - coordinates: [24, 36] - teleporter: [52, 8] -- name: Mac Ship Deck - North Stairs Script - id: 404 - area: 97 - coordinates: [12, 9] - teleporter: [53, 8] -- name: Mac Ship B1 Outer Ring - South Stairs - id: 405 - area: 98 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring - West Stairs - id: 406 - area: 98 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring - East Stairs - id: 407 - area: 98 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Outer Ring - Northwest Stairs - id: 408 - area: 98 - coordinates: [10, 23] - teleporter: [88, 0] -- name: Mac Ship B1 Square Room - North Stairs - id: 409 - area: 98 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room - South Stairs - id: 410 - area: 98 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room - Stairs # Unused? - id: 411 - area: 98 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor - South Stairs - id: 412 - area: 98 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor - North Stairs - id: 413 - area: 98 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B2 South Corridor - South Stairs - id: 414 - area: 99 - coordinates: [48, 51] - teleporter: [57, 1] -- name: Mac Ship B2 South Corridor - North Stairs Script - id: 415 - area: 99 - coordinates: [48, 38] - teleporter: [55, 8] -- name: Mac Ship B2 North Corridor - South Stairs Script - id: 416 - area: 99 - coordinates: [48, 27] - teleporter: [56, 8] -- name: Mac Ship B2 North Corridor - North Stairs Script - id: 417 - area: 99 - coordinates: [48, 12] - teleporter: [57, 8] -- name: Mac Ship B2 Outer Ring - Northwest Stairs Script - id: 418 - area: 99 - coordinates: [55, 11] - teleporter: [58, 8] -- name: Mac Ship B1 Outer Ring Cleared - South Stairs - id: 419 - area: 100 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring Cleared - West Stairs - id: 420 - area: 100 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring Cleared - East Stairs - id: 421 - area: 100 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Square Room Cleared - North Stairs - id: 422 - area: 100 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room Cleared - South Stairs - id: 423 - area: 100 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room Cleared - Main Stairs - id: 424 - area: 100 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor Cleared - South Stairs - id: 425 - area: 100 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor Cleared - North Stairs - id: 426 - area: 100 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs - id: 427 - area: 100 - coordinates: [23, 10] - teleporter: [88, 0] -- name: Doom Castle Corridor of Destiny - South Entrance - id: 428 - area: 101 - coordinates: [59, 29] - teleporter: [84, 0] -- name: Doom Castle Corridor of Destiny - Ice Floor Entrance - id: 429 - area: 101 - coordinates: [59, 21] - teleporter: [35, 2] -- name: Doom Castle Corridor of Destiny - Lava Floor Entrance - id: 430 - area: 101 - coordinates: [59, 13] - teleporter: [209, 0] -- name: Doom Castle Corridor of Destiny - Sky Floor Entrance - id: 431 - area: 101 - coordinates: [59, 5] - teleporter: [211, 0] -- name: Doom Castle Corridor of Destiny - Hero Room Entrance - id: 432 - area: 101 - coordinates: [59, 61] - teleporter: [13, 2] -- name: Doom Castle Ice Floor - Entrance - id: 433 - area: 102 - coordinates: [23, 42] - teleporter: [109, 3] -- name: Doom Castle Lava Floor - Entrance - id: 434 - area: 103 - coordinates: [23, 40] - teleporter: [210, 0] -- name: Doom Castle Sky Floor - Entrance - id: 435 - area: 104 - coordinates: [24, 41] - teleporter: [212, 0] -- name: Doom Castle Hero Room - Dark King Entrance 1 - id: 436 - area: 106 - coordinates: [15, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 2 - id: 437 - area: 106 - coordinates: [16, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 3 - id: 438 - area: 106 - coordinates: [15, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 4 - id: 439 - area: 106 - coordinates: [16, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Hero Statue Script - id: 440 - area: 106 - coordinates: [15, 17] - teleporter: [24, 8] -- name: Doom Castle Hero Room - Entrance - id: 441 - area: 106 - coordinates: [15, 24] - teleporter: [110, 3] -- name: Doom Castle Dark King Room - Entrance - id: 442 - area: 107 - coordinates: [14, 26] - teleporter: [52, 0] -- name: Doom Castle Dark King Room - Dark King Script - id: 443 - area: 107 - coordinates: [14, 15] - teleporter: [25, 8] -- name: Doom Castle Dark King Room - Unknown - id: 444 - area: 107 - coordinates: [47, 54] - teleporter: [77, 0] -- name: Overworld - Level Forest - id: 445 - area: 0 - type: "Overworld" - teleporter: [0x2E, 8] -- name: Overworld - Foresta - id: 446 - area: 0 - type: "Overworld" - teleporter: [0x02, 1] -- name: Overworld - Sand Temple - id: 447 - area: 0 - type: "Overworld" - teleporter: [0x03, 1] -- name: Overworld - Bone Dungeon - id: 448 - area: 0 - type: "Overworld" - teleporter: [0x04, 1] -- name: Overworld - Focus Tower Foresta - id: 449 - area: 0 - type: "Overworld" - teleporter: [0x05, 1] -- name: Overworld - Focus Tower Aquaria - id: 450 - area: 0 - type: "Overworld" - teleporter: [0x13, 1] -- name: Overworld - Libra Temple - id: 451 - area: 0 - type: "Overworld" - teleporter: [0x07, 1] -- name: Overworld - Aquaria - id: 452 - area: 0 - type: "Overworld" - teleporter: [0x08, 8] -- name: Overworld - Wintry Cave - id: 453 - area: 0 - type: "Overworld" - teleporter: [0x0A, 1] -- name: Overworld - Life Temple - id: 454 - area: 0 - type: "Overworld" - teleporter: [0x0B, 1] -- name: Overworld - Falls Basin - id: 455 - area: 0 - type: "Overworld" - teleporter: [0x0C, 1] -- name: Overworld - Ice Pyramid - id: 456 - area: 0 - type: "Overworld" - teleporter: [0x0D, 1] # Will be switched to a script -- name: Overworld - Spencer's Place - id: 457 - area: 0 - type: "Overworld" - teleporter: [0x30, 8] -- name: Overworld - Wintry Temple - id: 458 - area: 0 - type: "Overworld" - teleporter: [0x10, 1] -- name: Overworld - Focus Tower Frozen Strip - id: 459 - area: 0 - type: "Overworld" - teleporter: [0x11, 1] -- name: Overworld - Focus Tower Fireburg - id: 460 - area: 0 - type: "Overworld" - teleporter: [0x12, 1] -- name: Overworld - Fireburg - id: 461 - area: 0 - type: "Overworld" - teleporter: [0x14, 1] -- name: Overworld - Mine - id: 462 - area: 0 - type: "Overworld" - teleporter: [0x15, 1] -- name: Overworld - Sealed Temple - id: 463 - area: 0 - type: "Overworld" - teleporter: [0x16, 1] -- name: Overworld - Volcano - id: 464 - area: 0 - type: "Overworld" - teleporter: [0x17, 1] -- name: Overworld - Lava Dome - id: 465 - area: 0 - type: "Overworld" - teleporter: [0x18, 1] -- name: Overworld - Focus Tower Windia - id: 466 - area: 0 - type: "Overworld" - teleporter: [0x06, 1] -- name: Overworld - Rope Bridge - id: 467 - area: 0 - type: "Overworld" - teleporter: [0x19, 1] -- name: Overworld - Alive Forest - id: 468 - area: 0 - type: "Overworld" - teleporter: [0x1A, 1] -- name: Overworld - Giant Tree - id: 469 - area: 0 - type: "Overworld" - teleporter: [0x1B, 1] -- name: Overworld - Kaidge Temple - id: 470 - area: 0 - type: "Overworld" - teleporter: [0x1C, 1] -- name: Overworld - Windia - id: 471 - area: 0 - type: "Overworld" - teleporter: [0x1D, 1] -- name: Overworld - Windhole Temple - id: 472 - area: 0 - type: "Overworld" - teleporter: [0x1E, 1] -- name: Overworld - Mount Gale - id: 473 - area: 0 - type: "Overworld" - teleporter: [0x1F, 1] -- name: Overworld - Pazuzu Tower - id: 474 - area: 0 - type: "Overworld" - teleporter: [0x20, 1] -- name: Overworld - Ship Dock - id: 475 - area: 0 - type: "Overworld" - teleporter: [0x3E, 1] -- name: Overworld - Doom Castle - id: 476 - area: 0 - type: "Overworld" - teleporter: [0x21, 1] -- name: Overworld - Light Temple - id: 477 - area: 0 - type: "Overworld" - teleporter: [0x22, 1] -- name: Overworld - Mac Ship - id: 478 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Overworld - Mac Ship Doom - id: 479 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Dummy House - Bed Script - id: 480 - area: 17 - coordinates: [0x28, 0x38] - teleporter: [1, 8] -- name: Dummy House - Entrance - id: 481 - area: 17 - coordinates: [0x29, 0x3B] - teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.py b/worlds/ffmq/data/rooms.py new file mode 100644 index 000000000000..38634f107679 --- /dev/null +++ b/worlds/ffmq/data/rooms.py @@ -0,0 +1,2 @@ +rooms = [{'name': 'Overworld', 'id': 0, 'type': 'Overworld', 'game_objects': [], 'links': [{'target_room': 220, 'access': []}]}, {'name': 'Subregion Foresta', 'id': 220, 'type': 'Subregion', 'region': 'Foresta', 'game_objects': [{'name': 'Foresta South Battlefield', 'object_id': 1, 'location': 'ForestaSouthBattlefield', 'location_slot': 'ForestaSouthBattlefield', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Foresta West Battlefield', 'object_id': 2, 'location': 'ForestaWestBattlefield', 'location_slot': 'ForestaWestBattlefield', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Foresta East Battlefield', 'object_id': 3, 'location': 'ForestaEastBattlefield', 'location_slot': 'ForestaEastBattlefield', 'type': 'BattlefieldGp', 'access': []}], 'links': [{'target_room': 15, 'location': 'LevelForest', 'location_slot': 'LevelForest', 'entrance': 445, 'teleporter': [46, 8], 'access': []}, {'target_room': 16, 'location': 'Foresta', 'location_slot': 'Foresta', 'entrance': 446, 'teleporter': [2, 1], 'access': []}, {'target_room': 24, 'location': 'SandTemple', 'location_slot': 'SandTemple', 'entrance': 447, 'teleporter': [3, 1], 'access': []}, {'target_room': 25, 'location': 'BoneDungeon', 'location_slot': 'BoneDungeon', 'entrance': 448, 'teleporter': [4, 1], 'access': []}, {'target_room': 3, 'location': 'FocusTowerForesta', 'location_slot': 'FocusTowerForesta', 'entrance': 449, 'teleporter': [5, 1], 'access': []}, {'target_room': 221, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['RiverCoin']}, {'target_room': 226, 'access': ['SunCoin']}]}, {'name': 'Subregion Aquaria', 'id': 221, 'type': 'Subregion', 'region': 'Aquaria', 'game_objects': [{'name': 'South of Libra Temple Battlefield', 'object_id': 4, 'location': 'AquariaBattlefield01', 'location_slot': 'AquariaBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'East of Libra Temple Battlefield', 'object_id': 5, 'location': 'AquariaBattlefield02', 'location_slot': 'AquariaBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'South of Aquaria Battlefield', 'object_id': 6, 'location': 'AquariaBattlefield03', 'location_slot': 'AquariaBattlefield03', 'type': 'BattlefieldItem', 'access': []}, {'name': 'South of Wintry Cave Battlefield', 'object_id': 7, 'location': 'WintryBattlefield01', 'location_slot': 'WintryBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'West of Wintry Cave Battlefield', 'object_id': 8, 'location': 'WintryBattlefield02', 'location_slot': 'WintryBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Ice Pyramid Battlefield', 'object_id': 9, 'location': 'PyramidBattlefield01', 'location_slot': 'PyramidBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 10, 'location': 'FocusTowerAquaria', 'location_slot': 'FocusTowerAquaria', 'entrance': 450, 'teleporter': [19, 1], 'access': []}, {'target_room': 39, 'location': 'LibraTemple', 'location_slot': 'LibraTemple', 'entrance': 451, 'teleporter': [7, 1], 'access': []}, {'target_room': 40, 'location': 'Aquaria', 'location_slot': 'Aquaria', 'entrance': 452, 'teleporter': [8, 8], 'access': []}, {'target_room': 45, 'location': 'WintryCave', 'location_slot': 'WintryCave', 'entrance': 453, 'teleporter': [10, 1], 'access': []}, {'target_room': 52, 'location': 'FallsBasin', 'location_slot': 'FallsBasin', 'entrance': 455, 'teleporter': [12, 1], 'access': []}, {'target_room': 54, 'location': 'IcePyramid', 'location_slot': 'IcePyramid', 'entrance': 456, 'teleporter': [13, 1], 'access': []}, {'target_room': 220, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Life Temple', 'id': 222, 'type': 'Subregion', 'region': 'LifeTemple', 'game_objects': [], 'links': [{'target_room': 51, 'location': 'LifeTemple', 'location_slot': 'LifeTemple', 'entrance': 454, 'teleporter': [11, 1], 'access': []}]}, {'name': 'Subregion Frozen Fields', 'id': 223, 'type': 'Subregion', 'region': 'AquariaFrozenField', 'game_objects': [{'name': 'North of Libra Temple Battlefield', 'object_id': 10, 'location': 'LibraBattlefield01', 'location_slot': 'LibraBattlefield01', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Aquaria Frozen Field Battlefield', 'object_id': 11, 'location': 'LibraBattlefield02', 'location_slot': 'LibraBattlefield02', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 74, 'location': 'WintryTemple', 'location_slot': 'WintryTemple', 'entrance': 458, 'teleporter': [16, 1], 'access': []}, {'target_room': 14, 'location': 'FocusTowerFrozen', 'location_slot': 'FocusTowerFrozen', 'entrance': 459, 'teleporter': [17, 1], 'access': []}, {'target_room': 221, 'access': []}, {'target_room': 225, 'access': ['SummerAquaria', 'DualheadHydra']}]}, {'name': 'Subregion Fireburg', 'id': 224, 'type': 'Subregion', 'region': 'Fireburg', 'game_objects': [{'name': 'Path to Fireburg Southern Battlefield', 'object_id': 12, 'location': 'FireburgBattlefield01', 'location_slot': 'FireburgBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Path to Fireburg Central Battlefield', 'object_id': 13, 'location': 'FireburgBattlefield02', 'location_slot': 'FireburgBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Path to Fireburg Northern Battlefield', 'object_id': 14, 'location': 'FireburgBattlefield03', 'location_slot': 'FireburgBattlefield03', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Sealed Temple Battlefield', 'object_id': 15, 'location': 'MineBattlefield01', 'location_slot': 'MineBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Mine Battlefield', 'object_id': 16, 'location': 'MineBattlefield02', 'location_slot': 'MineBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Boulder Battlefield', 'object_id': 17, 'location': 'MineBattlefield03', 'location_slot': 'MineBattlefield03', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 13, 'location': 'FocusTowerFireburg', 'location_slot': 'FocusTowerFireburg', 'entrance': 460, 'teleporter': [18, 1], 'access': []}, {'target_room': 76, 'location': 'Fireburg', 'location_slot': 'Fireburg', 'entrance': 461, 'teleporter': [20, 1], 'access': []}, {'target_room': 84, 'location': 'Mine', 'location_slot': 'Mine', 'entrance': 462, 'teleporter': [21, 1], 'access': []}, {'target_room': 92, 'location': 'SealedTemple', 'location_slot': 'SealedTemple', 'entrance': 463, 'teleporter': [22, 1], 'access': []}, {'target_room': 93, 'location': 'Volcano', 'location_slot': 'Volcano', 'entrance': 464, 'teleporter': [23, 1], 'access': []}, {'target_room': 100, 'location': 'LavaDome', 'location_slot': 'LavaDome', 'entrance': 465, 'teleporter': [24, 1], 'access': []}, {'target_room': 220, 'access': ['RiverCoin']}, {'target_room': 221, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 225, 'access': ['DualheadHydra']}]}, {'name': 'Subregion Volcano Battlefield', 'id': 225, 'type': 'Subregion', 'region': 'VolcanoBattlefield', 'game_objects': [{'name': 'Volcano Battlefield', 'object_id': 18, 'location': 'VolcanoBattlefield01', 'location_slot': 'VolcanoBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 224, 'access': ['DualheadHydra']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Windia', 'id': 226, 'type': 'Subregion', 'region': 'Windia', 'game_objects': [{'name': 'Kaidge Temple Battlefield', 'object_id': 19, 'location': 'WindiaBattlefield01', 'location_slot': 'WindiaBattlefield01', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}, {'name': 'South of Windia Battlefield', 'object_id': 20, 'location': 'WindiaBattlefield02', 'location_slot': 'WindiaBattlefield02', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}], 'links': [{'target_room': 9, 'location': 'FocusTowerWindia', 'location_slot': 'FocusTowerWindia', 'entrance': 466, 'teleporter': [6, 1], 'access': []}, {'target_room': 123, 'location': 'RopeBridge', 'location_slot': 'RopeBridge', 'entrance': 467, 'teleporter': [25, 1], 'access': []}, {'target_room': 124, 'location': 'AliveForest', 'location_slot': 'AliveForest', 'entrance': 468, 'teleporter': [26, 1], 'access': []}, {'target_room': 125, 'location': 'GiantTree', 'location_slot': 'GiantTree', 'entrance': 469, 'teleporter': [27, 1], 'access': ['Barred']}, {'target_room': 152, 'location': 'KaidgeTemple', 'location_slot': 'KaidgeTemple', 'entrance': 470, 'teleporter': [28, 1], 'access': []}, {'target_room': 156, 'location': 'Windia', 'location_slot': 'Windia', 'entrance': 471, 'teleporter': [29, 1], 'access': []}, {'target_room': 154, 'location': 'WindholeTemple', 'location_slot': 'WindholeTemple', 'entrance': 472, 'teleporter': [30, 1], 'access': []}, {'target_room': 155, 'location': 'MountGale', 'location_slot': 'MountGale', 'entrance': 473, 'teleporter': [31, 1], 'access': []}, {'target_room': 166, 'location': 'PazuzusTower', 'location_slot': 'PazuzusTower', 'entrance': 474, 'teleporter': [32, 1], 'access': []}, {'target_room': 220, 'access': ['SunCoin']}, {'target_room': 221, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 224, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 227, 'access': ['RainbowBridge']}]}, {'name': "Subregion Spencer's Cave", 'id': 227, 'type': 'Subregion', 'region': 'SpencerCave', 'game_objects': [], 'links': [{'target_room': 73, 'location': 'SpencersPlace', 'location_slot': 'SpencersPlace', 'entrance': 457, 'teleporter': [48, 8], 'access': []}, {'target_room': 226, 'access': ['RainbowBridge']}]}, {'name': 'Subregion Ship Dock', 'id': 228, 'type': 'Subregion', 'region': 'ShipDock', 'game_objects': [], 'links': [{'target_room': 186, 'location': 'ShipDock', 'location_slot': 'ShipDock', 'entrance': 475, 'teleporter': [62, 1], 'access': []}, {'target_room': 229, 'access': ['ShipLiberated', 'ShipDockAccess']}]}, {'name': "Subregion Mac's Ship", 'id': 229, 'type': 'Subregion', 'region': 'MacShip', 'game_objects': [], 'links': [{'target_room': 187, 'location': 'MacsShip', 'location_slot': 'MacsShip', 'entrance': 478, 'teleporter': [36, 1], 'access': []}, {'target_room': 228, 'access': ['ShipLiberated', 'ShipDockAccess']}, {'target_room': 231, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Subregion Light Temple', 'id': 230, 'type': 'Subregion', 'region': 'LightTemple', 'game_objects': [], 'links': [{'target_room': 185, 'location': 'LightTemple', 'location_slot': 'LightTemple', 'entrance': 477, 'teleporter': [35, 1], 'access': []}]}, {'name': 'Subregion Doom Castle', 'id': 231, 'type': 'Subregion', 'region': 'DoomCastle', 'game_objects': [], 'links': [{'target_room': 1, 'location': 'DoomCastle', 'location_slot': 'DoomCastle', 'entrance': 476, 'teleporter': [33, 1], 'access': []}, {'target_room': 187, 'location': 'MacsShipDoom', 'location_slot': 'MacsShipDoom', 'entrance': 479, 'teleporter': [36, 1], 'access': ['Barred']}, {'target_room': 229, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Doom Castle - Sand Floor', 'id': 1, 'game_objects': [{'name': 'Doom Castle B2 - Southeast Chest', 'object_id': 1, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Doom Castle B2 - Bone Ledge Box', 'object_id': 30, 'type': 'Box', 'access': []}, {'name': 'Doom Castle B2 - Hook Platform Box', 'object_id': 31, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 231, 'entrance': 1, 'teleporter': [1, 6], 'access': []}, {'target_room': 5, 'entrance': 0, 'teleporter': [0, 0], 'access': ['DragonClaw', 'MegaGrenade']}]}, {'name': 'Doom Castle - Aero Room', 'id': 2, 'game_objects': [{'name': 'Doom Castle B2 - Sun Door Chest', 'object_id': 0, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 4, 'entrance': 2, 'teleporter': [1, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Main Loop', 'id': 3, 'game_objects': [], 'links': [{'target_room': 220, 'entrance': 3, 'teleporter': [2, 6], 'access': []}, {'target_room': 6, 'entrance': 4, 'teleporter': [4, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Aero Corridor', 'id': 4, 'game_objects': [], 'links': [{'target_room': 9, 'entrance': 5, 'teleporter': [5, 0], 'access': []}, {'target_room': 2, 'entrance': 6, 'teleporter': [8, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Inner Loop', 'id': 5, 'game_objects': [], 'links': [{'target_room': 1, 'entrance': 8, 'teleporter': [7, 0], 'access': []}, {'target_room': 201, 'entrance': 7, 'teleporter': [6, 0], 'access': []}]}, {'name': 'Focus Tower 1F Main Lobby', 'id': 6, 'game_objects': [{'name': 'Focus Tower 1F - Main Lobby Box', 'object_id': 33, 'type': 'Box', 'access': []}], 'links': [{'target_room': 3, 'entrance': 11, 'teleporter': [11, 0], 'access': []}, {'target_room': 7, 'access': ['SandCoin']}, {'target_room': 8, 'access': ['RiverCoin']}, {'target_room': 9, 'access': ['SunCoin']}]}, {'name': 'Focus Tower 1F SandCoin Room', 'id': 7, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SandCoin']}, {'target_room': 10, 'entrance': 10, 'teleporter': [10, 0], 'access': []}]}, {'name': 'Focus Tower 1F RiverCoin Room', 'id': 8, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['RiverCoin']}, {'target_room': 11, 'entrance': 14, 'teleporter': [14, 0], 'access': []}]}, {'name': 'Focus Tower 1F SunCoin Room', 'id': 9, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SunCoin']}, {'target_room': 4, 'entrance': 12, 'teleporter': [12, 0], 'access': []}, {'target_room': 226, 'entrance': 9, 'teleporter': [3, 6], 'access': []}]}, {'name': 'Focus Tower 1F SkyCoin Room', 'id': 201, 'game_objects': [], 'links': [{'target_room': 195, 'entrance': 13, 'teleporter': [13, 0], 'access': ['SkyCoin', 'FlamerusRex', 'IceGolem', 'DualheadHydra', 'Pazuzu']}, {'target_room': 5, 'entrance': 15, 'teleporter': [15, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Sand Coin Passage', 'id': 10, 'game_objects': [{'name': 'Focus Tower 2F - Sand Door Chest', 'object_id': 3, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 221, 'entrance': 16, 'teleporter': [4, 6], 'access': []}, {'target_room': 7, 'entrance': 17, 'teleporter': [17, 0], 'access': []}]}, {'name': 'Focus Tower 2F - River Coin Passage', 'id': 11, 'game_objects': [], 'links': [{'target_room': 8, 'entrance': 18, 'teleporter': [18, 0], 'access': []}, {'target_room': 13, 'entrance': 19, 'teleporter': [20, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Venus Chest Room', 'id': 12, 'game_objects': [{'name': 'Focus Tower 2F - Back Door Chest', 'object_id': 2, 'type': 'Chest', 'access': []}, {'name': 'Focus Tower 2F - Venus Chest', 'object_id': 9, 'type': 'NPC', 'access': ['Bomb', 'VenusKey']}], 'links': [{'target_room': 14, 'entrance': 20, 'teleporter': [19, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Lower Floor', 'id': 13, 'game_objects': [{'name': 'Focus Tower 3F - River Door Box', 'object_id': 34, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 22, 'teleporter': [6, 6], 'access': []}, {'target_room': 11, 'entrance': 23, 'teleporter': [24, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Upper Floor', 'id': 14, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 24, 'teleporter': [5, 6], 'access': []}, {'target_room': 12, 'entrance': 25, 'teleporter': [23, 0], 'access': []}]}, {'name': 'Level Forest', 'id': 15, 'game_objects': [{'name': 'Level Forest - Northwest Box', 'object_id': 40, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Northeast Box', 'object_id': 41, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Middle Box', 'object_id': 42, 'type': 'Box', 'access': []}, {'name': 'Level Forest - Southwest Box', 'object_id': 43, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Southeast Box', 'object_id': 44, 'type': 'Box', 'access': ['Axe']}, {'name': 'Minotaur', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Minotaur'], 'access': ['Kaeli1']}, {'name': 'Level Forest - Old Man', 'object_id': 0, 'type': 'NPC', 'access': []}, {'name': 'Level Forest - Kaeli', 'object_id': 1, 'type': 'NPC', 'access': ['Kaeli1', 'Minotaur']}], 'links': [{'target_room': 220, 'entrance': 28, 'teleporter': [25, 0], 'access': []}]}, {'name': 'Foresta', 'id': 16, 'game_objects': [{'name': 'Foresta - Outside Box', 'object_id': 45, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 220, 'entrance': 38, 'teleporter': [31, 0], 'access': []}, {'target_room': 17, 'entrance': 44, 'teleporter': [0, 5], 'access': []}, {'target_room': 18, 'entrance': 42, 'teleporter': [32, 4], 'access': []}, {'target_room': 19, 'entrance': 43, 'teleporter': [33, 0], 'access': []}, {'target_room': 20, 'entrance': 45, 'teleporter': [1, 5], 'access': []}]}, {'name': "Kaeli's House", 'id': 17, 'game_objects': [{'name': "Foresta - Kaeli's House Box", 'object_id': 46, 'type': 'Box', 'access': []}, {'name': 'Kaeli Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli1'], 'access': ['TreeWither']}, {'name': 'Kaeli 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli2'], 'access': ['Kaeli1', 'Minotaur', 'Elixir']}], 'links': [{'target_room': 16, 'entrance': 46, 'teleporter': [86, 3], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Main", 'id': 18, 'game_objects': [], 'links': [{'target_room': 19, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 47, 'teleporter': [34, 0], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Back", 'id': 19, 'game_objects': [{'name': 'Foresta - Old Man House Chest', 'object_id': 5, 'type': 'Chest', 'access': []}, {'name': 'Old Man Barrel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['BarrelPushed'], 'access': []}], 'links': [{'target_room': 18, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 48, 'teleporter': [35, 0], 'access': []}]}, {'name': 'Foresta Houses - Rest House', 'id': 20, 'game_objects': [{'name': 'Foresta - Rest House Box', 'object_id': 47, 'type': 'Box', 'access': []}], 'links': [{'target_room': 16, 'entrance': 50, 'teleporter': [87, 3], 'access': []}]}, {'name': 'Libra Treehouse', 'id': 21, 'game_objects': [{'name': 'Alive Forest - Libra Treehouse Box', 'object_id': 50, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 51, 'teleporter': [67, 8], 'access': ['LibraCrest']}]}, {'name': 'Gemini Treehouse', 'id': 22, 'game_objects': [{'name': 'Alive Forest - Gemini Treehouse Box', 'object_id': 51, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 52, 'teleporter': [68, 8], 'access': ['GeminiCrest']}]}, {'name': 'Mobius Treehouse', 'id': 23, 'game_objects': [{'name': 'Alive Forest - Mobius Treehouse West Box', 'object_id': 48, 'type': 'Box', 'access': []}, {'name': 'Alive Forest - Mobius Treehouse East Box', 'object_id': 49, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 53, 'teleporter': [69, 8], 'access': ['MobiusCrest']}]}, {'name': 'Sand Temple', 'id': 24, 'game_objects': [{'name': 'Tristam Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Tristam'], 'access': []}], 'links': [{'target_room': 220, 'entrance': 54, 'teleporter': [36, 0], 'access': []}]}, {'name': 'Bone Dungeon 1F', 'id': 25, 'game_objects': [{'name': 'Bone Dungeon 1F - Entrance Room West Box', 'object_id': 53, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room Middle Box', 'object_id': 54, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room East Box', 'object_id': 55, 'type': 'Box', 'access': []}], 'links': [{'target_room': 220, 'entrance': 55, 'teleporter': [37, 0], 'access': []}, {'target_room': 26, 'entrance': 56, 'teleporter': [2, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Waterway', 'id': 26, 'game_objects': [{'name': 'Bone Dungeon B1 - Skull Chest', 'object_id': 6, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Bone Dungeon B1 - Tristam', 'object_id': 2, 'type': 'NPC', 'access': ['Tristam']}, {'name': 'Tristam Bone Dungeon Item Given', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TristamBoneItemGiven'], 'access': ['Tristam']}], 'links': [{'target_room': 25, 'entrance': 59, 'teleporter': [88, 3], 'access': []}, {'target_room': 28, 'entrance': 57, 'teleporter': [3, 2], 'access': ['Bomb']}]}, {'name': 'Bone Dungeon B1 - Checker Room', 'id': 28, 'game_objects': [{'name': 'Bone Dungeon B1 - Checker Room Box', 'object_id': 56, 'type': 'Box', 'access': ['Bomb']}], 'links': [{'target_room': 26, 'entrance': 61, 'teleporter': [89, 3], 'access': []}, {'target_room': 30, 'entrance': 60, 'teleporter': [4, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Hidden Room', 'id': 29, 'game_objects': [{'name': 'Bone Dungeon B1 - Ribcage Waterway Box', 'object_id': 57, 'type': 'Box', 'access': []}], 'links': [{'target_room': 31, 'entrance': 62, 'teleporter': [91, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - First Room', 'id': 30, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Alcove Box', 'object_id': 59, 'type': 'Box', 'access': []}, {'name': 'Long Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LongSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 28, 'entrance': 65, 'teleporter': [90, 3], 'access': []}, {'target_room': 31, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Second Room', 'id': 31, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Looped Hallway Box', 'object_id': 58, 'type': 'Box', 'access': []}, {'name': 'Short Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShortSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 29, 'entrance': 63, 'teleporter': [5, 2], 'access': ['LongSpineBombed']}, {'target_room': 32, 'access': ['ShortSpineBombed']}, {'target_room': 30, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Third Room', 'id': 32, 'game_objects': [], 'links': [{'target_room': 35, 'entrance': 64, 'teleporter': [6, 2], 'access': []}, {'target_room': 31, 'access': ['ShortSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Box Room', 'id': 33, 'game_objects': [{'name': 'Bone Dungeon B2 - Lone Room Box', 'object_id': 61, 'type': 'Box', 'access': []}], 'links': [{'target_room': 36, 'entrance': 66, 'teleporter': [93, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Quake Room', 'id': 34, 'game_objects': [{'name': 'Bone Dungeon B2 - Penultimate Room Chest', 'object_id': 7, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 37, 'entrance': 67, 'teleporter': [94, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - First Room', 'id': 35, 'game_objects': [{'name': 'Bone Dungeon B2 - Two Skulls Room Box', 'object_id': 60, 'type': 'Box', 'access': []}, {'name': 'Skull 1', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull1Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 32, 'entrance': 71, 'teleporter': [92, 3], 'access': []}, {'target_room': 36, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Second Room', 'id': 36, 'game_objects': [{'name': 'Skull 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull2Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 33, 'entrance': 68, 'teleporter': [7, 2], 'access': []}, {'target_room': 37, 'access': ['Skull2Bombed']}, {'target_room': 35, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Third Room', 'id': 37, 'game_objects': [], 'links': [{'target_room': 34, 'entrance': 69, 'teleporter': [8, 2], 'access': []}, {'target_room': 38, 'entrance': 70, 'teleporter': [9, 2], 'access': ['Bomb']}, {'target_room': 36, 'access': ['Skull2Bombed']}]}, {'name': 'Bone Dungeon B2 - Boss Room', 'id': 38, 'game_objects': [{'name': 'Bone Dungeon B2 - North Box', 'object_id': 62, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - South Box', 'object_id': 63, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - Flamerus Rex Chest', 'object_id': 8, 'type': 'Chest', 'access': []}, {'name': "Bone Dungeon B2 - Tristam's Treasure Chest", 'object_id': 4, 'type': 'Chest', 'access': []}, {'name': 'Flamerus Rex', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FlamerusRex'], 'access': []}], 'links': [{'target_room': 37, 'entrance': 74, 'teleporter': [95, 3], 'access': []}]}, {'name': 'Libra Temple', 'id': 39, 'game_objects': [{'name': 'Libra Temple - Box', 'object_id': 64, 'type': 'Box', 'access': []}, {'name': 'Phoebe Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phoebe1'], 'access': []}], 'links': [{'target_room': 221, 'entrance': 75, 'teleporter': [13, 6], 'access': []}, {'target_room': 51, 'entrance': 76, 'teleporter': [59, 8], 'access': ['LibraCrest']}]}, {'name': 'Aquaria', 'id': 40, 'game_objects': [{'name': 'Summer Aquaria', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SummerAquaria'], 'access': ['WakeWater']}], 'links': [{'target_room': 221, 'entrance': 77, 'teleporter': [8, 6], 'access': []}, {'target_room': 41, 'entrance': 81, 'teleporter': [10, 5], 'access': []}, {'target_room': 42, 'entrance': 82, 'teleporter': [44, 4], 'access': []}, {'target_room': 44, 'entrance': 83, 'teleporter': [11, 5], 'access': []}, {'target_room': 71, 'entrance': 89, 'teleporter': [42, 0], 'access': ['SummerAquaria']}, {'target_room': 71, 'entrance': 90, 'teleporter': [43, 0], 'access': ['SummerAquaria']}]}, {'name': "Phoebe's House", 'id': 41, 'game_objects': [{'name': "Aquaria - Phoebe's House Chest", 'object_id': 65, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 93, 'teleporter': [5, 8], 'access': []}]}, {'name': 'Aquaria Vendor House', 'id': 42, 'game_objects': [{'name': 'Aquaria - Vendor', 'object_id': 4, 'type': 'NPC', 'access': []}, {'name': 'Aquaria - Vendor House Box', 'object_id': 66, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 94, 'teleporter': [40, 8], 'access': []}, {'target_room': 43, 'entrance': 95, 'teleporter': [47, 0], 'access': []}]}, {'name': 'Aquaria Gemini Room', 'id': 43, 'game_objects': [], 'links': [{'target_room': 42, 'entrance': 97, 'teleporter': [48, 0], 'access': []}, {'target_room': 81, 'entrance': 96, 'teleporter': [72, 8], 'access': ['GeminiCrest']}]}, {'name': 'Aquaria INN', 'id': 44, 'game_objects': [], 'links': [{'target_room': 40, 'entrance': 98, 'teleporter': [75, 8], 'access': []}]}, {'name': 'Wintry Cave 1F - East Ledge', 'id': 45, 'game_objects': [{'name': 'Wintry Cave 1F - North Box', 'object_id': 67, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Entrance Box', 'object_id': 70, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Slippery Cliff Box', 'object_id': 68, 'type': 'Box', 'access': ['Claw']}, {'name': 'Wintry Cave 1F - Phoebe', 'object_id': 5, 'type': 'NPC', 'access': ['Phoebe1']}], 'links': [{'target_room': 221, 'entrance': 99, 'teleporter': [49, 0], 'access': []}, {'target_room': 49, 'entrance': 100, 'teleporter': [14, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - Central Space', 'id': 46, 'game_objects': [{'name': 'Wintry Cave 1F - Scenic Overlook Box', 'object_id': 69, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 45, 'access': ['Claw']}, {'target_room': 47, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - West Ledge', 'id': 47, 'game_objects': [], 'links': [{'target_room': 48, 'entrance': 101, 'teleporter': [15, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 2F', 'id': 48, 'game_objects': [{'name': 'Wintry Cave 2F - West Left Box', 'object_id': 71, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - West Right Box', 'object_id': 72, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Left Box', 'object_id': 73, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Right Box', 'object_id': 74, 'type': 'Box', 'access': []}], 'links': [{'target_room': 47, 'entrance': 104, 'teleporter': [97, 3], 'access': []}, {'target_room': 50, 'entrance': 103, 'teleporter': [50, 0], 'access': []}]}, {'name': 'Wintry Cave 3F Top', 'id': 49, 'game_objects': [{'name': 'Wintry Cave 3F - West Box', 'object_id': 75, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 3F - East Box', 'object_id': 76, 'type': 'Box', 'access': []}], 'links': [{'target_room': 45, 'entrance': 105, 'teleporter': [96, 3], 'access': []}]}, {'name': 'Wintry Cave 3F Bottom', 'id': 50, 'game_objects': [{'name': 'Wintry Cave 3F - Squidite Chest', 'object_id': 9, 'type': 'Chest', 'access': ['Phanquid']}, {'name': 'Phanquid', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phanquid'], 'access': []}, {'name': 'Wintry Cave 3F - Before Boss Box', 'object_id': 77, 'type': 'Box', 'access': []}], 'links': [{'target_room': 48, 'entrance': 106, 'teleporter': [51, 0], 'access': []}]}, {'name': 'Life Temple', 'id': 51, 'game_objects': [{'name': 'Life Temple - Box', 'object_id': 78, 'type': 'Box', 'access': []}, {'name': 'Life Temple - Mysterious Man', 'object_id': 6, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 222, 'entrance': 107, 'teleporter': [14, 6], 'access': []}, {'target_room': 39, 'entrance': 108, 'teleporter': [60, 8], 'access': ['LibraCrest']}]}, {'name': 'Fall Basin', 'id': 52, 'game_objects': [{'name': 'Falls Basin - Snow Crab Chest', 'object_id': 10, 'type': 'Chest', 'access': ['FreezerCrab']}, {'name': 'Freezer Crab', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FreezerCrab'], 'access': []}, {'name': 'Falls Basin - Box', 'object_id': 79, 'type': 'Box', 'access': []}], 'links': [{'target_room': 221, 'entrance': 111, 'teleporter': [53, 0], 'access': []}]}, {'name': 'Ice Pyramid B1 Taunt Room', 'id': 53, 'game_objects': [{'name': 'Ice Pyramid B1 - Chest', 'object_id': 11, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid B1 - West Box', 'object_id': 80, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - North Box', 'object_id': 81, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - East Box', 'object_id': 82, 'type': 'Box', 'access': []}], 'links': [{'target_room': 68, 'entrance': 113, 'teleporter': [55, 0], 'access': []}]}, {'name': 'Ice Pyramid 1F Maze Lobby', 'id': 54, 'game_objects': [{'name': 'Ice Pyramid 1F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid1FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 221, 'entrance': 114, 'teleporter': [56, 0], 'access': []}, {'target_room': 55, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 1F Maze', 'id': 55, 'game_objects': [{'name': 'Ice Pyramid 1F - East Alcove Chest', 'object_id': 13, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 1F - Sandwiched Alcove Box', 'object_id': 83, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Left Box', 'object_id': 84, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Right Box', 'object_id': 85, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 116, 'teleporter': [57, 0], 'access': []}, {'target_room': 57, 'entrance': 117, 'teleporter': [58, 0], 'access': []}, {'target_room': 58, 'entrance': 118, 'teleporter': [59, 0], 'access': []}, {'target_room': 59, 'entrance': 119, 'teleporter': [60, 0], 'access': []}, {'target_room': 60, 'entrance': 120, 'teleporter': [61, 0], 'access': []}, {'target_room': 54, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 2F South Tiled Room', 'id': 56, 'game_objects': [{'name': 'Ice Pyramid 2F - South Side Glass Door Box', 'object_id': 87, 'type': 'Box', 'access': ['Sword']}, {'name': 'Ice Pyramid 2F - South Side East Box', 'object_id': 91, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 122, 'teleporter': [62, 0], 'access': []}, {'target_room': 61, 'entrance': 123, 'teleporter': [67, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F West Room', 'id': 57, 'game_objects': [{'name': 'Ice Pyramid 2F - Northwest Room Box', 'object_id': 90, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 124, 'teleporter': [63, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Center Room', 'id': 58, 'game_objects': [{'name': 'Ice Pyramid 2F - Center Room Box', 'object_id': 86, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 125, 'teleporter': [64, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Small North Room', 'id': 59, 'game_objects': [{'name': 'Ice Pyramid 2F - North Room Glass Door Box', 'object_id': 88, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 126, 'teleporter': [65, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F North Corridor', 'id': 60, 'game_objects': [{'name': 'Ice Pyramid 2F - North Corridor Glass Door Box', 'object_id': 89, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 127, 'teleporter': [66, 0], 'access': []}, {'target_room': 62, 'entrance': 128, 'teleporter': [68, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Two Boxes Room', 'id': 61, 'game_objects': [{'name': 'Ice Pyramid 3F - Staircase Dead End Left Box', 'object_id': 94, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Staircase Dead End Right Box', 'object_id': 95, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 129, 'teleporter': [69, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Main Loop', 'id': 62, 'game_objects': [{'name': 'Ice Pyramid 3F - Inner Room North Box', 'object_id': 92, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Inner Room South Box', 'object_id': 93, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - East Alcove Box', 'object_id': 96, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Leapfrog Box', 'object_id': 97, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid3FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 60, 'entrance': 130, 'teleporter': [70, 0], 'access': []}, {'target_room': 63, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 3F Blocked Room', 'id': 63, 'game_objects': [], 'links': [{'target_room': 64, 'entrance': 131, 'teleporter': [71, 0], 'access': []}, {'target_room': 62, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 4F Main Loop', 'id': 64, 'game_objects': [], 'links': [{'target_room': 66, 'entrance': 133, 'teleporter': [73, 0], 'access': []}, {'target_room': 63, 'entrance': 132, 'teleporter': [72, 0], 'access': []}, {'target_room': 65, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 4F Treasure Room', 'id': 65, 'game_objects': [{'name': 'Ice Pyramid 4F - Chest', 'object_id': 12, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 4F - Northwest Box', 'object_id': 98, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Left Box', 'object_id': 99, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Right Box', 'object_id': 100, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Left Box', 'object_id': 101, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Right Box', 'object_id': 102, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Left Box', 'object_id': 103, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Right Box', 'object_id': 104, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid4FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 64, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 5F Leap of Faith Room', 'id': 66, 'game_objects': [{'name': 'Ice Pyramid 5F - Glass Door Left Box', 'object_id': 105, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - West Ledge Box', 'object_id': 106, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Shelf Box', 'object_id': 107, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Leapfrog Box', 'object_id': 108, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - Glass Door Right Box', 'object_id': 109, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - North Box', 'object_id': 110, 'type': 'Box', 'access': []}], 'links': [{'target_room': 64, 'entrance': 134, 'teleporter': [74, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 53, 'access': ['Bomb', 'Claw', 'Sword']}]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem', 'id': 67, 'game_objects': [{'name': 'Ice Pyramid 5F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid5FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 69, 'entrance': 137, 'teleporter': [76, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 70, 'entrance': 136, 'teleporter': [75, 0], 'access': []}]}, {'name': 'Ice Pyramid Climbing Wall Room Lower Space', 'id': 68, 'game_objects': [], 'links': [{'target_room': 53, 'entrance': 139, 'teleporter': [78, 0], 'access': []}, {'target_room': 69, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Climbing Wall Room Upper Space', 'id': 69, 'game_objects': [], 'links': [{'target_room': 67, 'entrance': 140, 'teleporter': [79, 0], 'access': []}, {'target_room': 68, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Ice Golem Room', 'id': 70, 'game_objects': [{'name': 'Ice Pyramid 6F - Ice Golem Chest', 'object_id': 14, 'type': 'Chest', 'access': ['IceGolem']}, {'name': 'Ice Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IceGolem'], 'access': []}], 'links': [{'target_room': 67, 'entrance': 141, 'teleporter': [80, 0], 'access': []}, {'target_room': 66, 'access': []}]}, {'name': 'Spencer Waterfall', 'id': 71, 'game_objects': [], 'links': [{'target_room': 72, 'entrance': 143, 'teleporter': [81, 0], 'access': []}, {'target_room': 40, 'entrance': 145, 'teleporter': [82, 0], 'access': []}, {'target_room': 40, 'entrance': 148, 'teleporter': [83, 0], 'access': []}]}, {'name': 'Spencer Cave Normal Main', 'id': 72, 'game_objects': [{'name': "Spencer's Cave - Box", 'object_id': 111, 'type': 'Box', 'access': ['Claw']}, {'name': "Spencer's Cave - Spencer", 'object_id': 8, 'type': 'NPC', 'access': []}, {'name': "Spencer's Cave - Locked Chest", 'object_id': 13, 'type': 'NPC', 'access': ['VenusKey']}], 'links': [{'target_room': 71, 'entrance': 150, 'teleporter': [85, 0], 'access': []}]}, {'name': 'Spencer Cave Normal South Ledge', 'id': 73, 'game_objects': [{'name': "Collapse Spencer's Cave", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLiberated'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 227, 'entrance': 151, 'teleporter': [7, 6], 'access': []}, {'target_room': 203, 'access': ['MegaGrenade']}]}, {'name': 'Spencer Cave Caved In Main Loop', 'id': 203, 'game_objects': [], 'links': [{'target_room': 73, 'access': []}, {'target_room': 207, 'entrance': 156, 'teleporter': [36, 8], 'access': ['MobiusCrest']}, {'target_room': 204, 'access': ['Claw']}, {'target_room': 205, 'access': ['Bomb']}]}, {'name': 'Spencer Cave Caved In Waters', 'id': 204, 'game_objects': [{'name': 'Bomb Libra Block', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SpencerCaveLibraBlockBombed'], 'access': ['MegaGrenade', 'Claw']}], 'links': [{'target_room': 203, 'access': ['Claw']}]}, {'name': 'Spencer Cave Caved In Libra Nook', 'id': 205, 'game_objects': [], 'links': [{'target_room': 206, 'entrance': 153, 'teleporter': [33, 8], 'access': ['LibraCrest']}]}, {'name': 'Spencer Cave Caved In Libra Corridor', 'id': 206, 'game_objects': [], 'links': [{'target_room': 205, 'entrance': 154, 'teleporter': [34, 8], 'access': ['LibraCrest']}, {'target_room': 207, 'access': ['SpencerCaveLibraBlockBombed']}]}, {'name': 'Spencer Cave Caved In Mobius Chest', 'id': 207, 'game_objects': [{'name': "Spencer's Cave - Mobius Chest", 'object_id': 15, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 203, 'entrance': 155, 'teleporter': [35, 8], 'access': ['MobiusCrest']}, {'target_room': 206, 'access': ['Bomb']}]}, {'name': 'Wintry Temple Outer Room', 'id': 74, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 157, 'teleporter': [15, 6], 'access': []}]}, {'name': 'Wintry Temple Inner Room', 'id': 75, 'game_objects': [{'name': 'Wintry Temple - West Box', 'object_id': 112, 'type': 'Box', 'access': []}, {'name': 'Wintry Temple - North Box', 'object_id': 113, 'type': 'Box', 'access': []}], 'links': [{'target_room': 92, 'entrance': 158, 'teleporter': [62, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Upper Plaza', 'id': 76, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 159, 'teleporter': [9, 6], 'access': []}, {'target_room': 80, 'entrance': 163, 'teleporter': [91, 0], 'access': []}, {'target_room': 77, 'entrance': 164, 'teleporter': [98, 8], 'access': []}, {'target_room': 82, 'entrance': 165, 'teleporter': [96, 8], 'access': []}, {'target_room': 208, 'access': ['Claw']}]}, {'name': 'Fireburg Lower Plaza', 'id': 208, 'game_objects': [{'name': 'Fireburg - Hidden Tunnel Box', 'object_id': 116, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'access': ['Claw']}, {'target_room': 78, 'entrance': 166, 'teleporter': [11, 8], 'access': ['MultiKey']}]}, {'name': "Reuben's House", 'id': 77, 'game_objects': [{'name': "Fireburg - Reuben's House Arion", 'object_id': 14, 'type': 'NPC', 'access': ['ReubenDadSaved']}, {'name': 'Reuben Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Reuben1'], 'access': []}, {'name': "Fireburg - Reuben's House Box", 'object_id': 117, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'entrance': 167, 'teleporter': [98, 3], 'access': []}]}, {'name': "GrenadeMan's House", 'id': 78, 'game_objects': [{'name': 'Fireburg - Locked House Man', 'object_id': 12, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 208, 'entrance': 168, 'teleporter': [9, 8], 'access': ['MultiKey']}, {'target_room': 79, 'entrance': 169, 'teleporter': [93, 0], 'access': []}]}, {'name': "GrenadeMan's Mobius Room", 'id': 79, 'game_objects': [], 'links': [{'target_room': 78, 'entrance': 170, 'teleporter': [94, 0], 'access': []}, {'target_room': 161, 'entrance': 171, 'teleporter': [54, 8], 'access': ['MobiusCrest']}]}, {'name': 'Fireburg Vendor House', 'id': 80, 'game_objects': [{'name': 'Fireburg - Vendor', 'object_id': 11, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 76, 'entrance': 172, 'teleporter': [95, 0], 'access': []}, {'target_room': 81, 'entrance': 173, 'teleporter': [96, 0], 'access': []}]}, {'name': 'Fireburg Gemini Room', 'id': 81, 'game_objects': [], 'links': [{'target_room': 80, 'entrance': 174, 'teleporter': [97, 0], 'access': []}, {'target_room': 43, 'entrance': 175, 'teleporter': [45, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Hotel Lobby', 'id': 82, 'game_objects': [{'name': 'Fireburg - Tristam', 'object_id': 10, 'type': 'NPC', 'access': ['Tristam', 'TristamBoneItemGiven']}], 'links': [{'target_room': 76, 'entrance': 177, 'teleporter': [99, 3], 'access': []}, {'target_room': 83, 'entrance': 176, 'teleporter': [213, 0], 'access': []}]}, {'name': 'Fireburg Hotel Beds', 'id': 83, 'game_objects': [], 'links': [{'target_room': 82, 'entrance': 178, 'teleporter': [214, 0], 'access': []}]}, {'name': 'Mine Exterior North West Platforms', 'id': 84, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 179, 'teleporter': [98, 0], 'access': []}, {'target_room': 88, 'entrance': 181, 'teleporter': [20, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}, {'target_room': 86, 'access': ['Claw']}, {'target_room': 87, 'access': ['Claw']}]}, {'name': 'Mine Exterior Central Ledge', 'id': 85, 'game_objects': [], 'links': [{'target_room': 90, 'entrance': 183, 'teleporter': [22, 2], 'access': ['Bomb']}, {'target_room': 84, 'access': ['Claw']}]}, {'name': 'Mine Exterior North Ledge', 'id': 86, 'game_objects': [], 'links': [{'target_room': 89, 'entrance': 182, 'teleporter': [21, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Exterior South East Platforms', 'id': 87, 'game_objects': [{'name': 'Jinn', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Jinn'], 'access': []}], 'links': [{'target_room': 91, 'entrance': 180, 'teleporter': [99, 0], 'access': ['Jinn']}, {'target_room': 86, 'access': []}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Parallel Room', 'id': 88, 'game_objects': [{'name': 'Mine - Parallel Room West Box', 'object_id': 119, 'type': 'Box', 'access': ['Claw']}, {'name': 'Mine - Parallel Room East Box', 'object_id': 120, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 84, 'entrance': 185, 'teleporter': [100, 3], 'access': []}]}, {'name': 'Mine Crescent Room', 'id': 89, 'game_objects': [{'name': 'Mine - Crescent Room Chest', 'object_id': 16, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 86, 'entrance': 186, 'teleporter': [101, 3], 'access': []}]}, {'name': 'Mine Climbing Room', 'id': 90, 'game_objects': [{'name': 'Mine - Glitchy Collision Cave Box', 'object_id': 118, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 85, 'entrance': 187, 'teleporter': [102, 3], 'access': []}]}, {'name': 'Mine Cliff', 'id': 91, 'game_objects': [{'name': 'Mine - Cliff Southwest Box', 'object_id': 121, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northwest Box', 'object_id': 122, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northeast Box', 'object_id': 123, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Southeast Box', 'object_id': 124, 'type': 'Box', 'access': []}, {'name': 'Mine - Reuben', 'object_id': 7, 'type': 'NPC', 'access': ['Reuben1']}, {'name': "Reuben's dad Saved", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ReubenDadSaved'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 87, 'entrance': 188, 'teleporter': [100, 0], 'access': []}]}, {'name': 'Sealed Temple', 'id': 92, 'game_objects': [{'name': 'Sealed Temple - West Box', 'object_id': 125, 'type': 'Box', 'access': []}, {'name': 'Sealed Temple - East Box', 'object_id': 126, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 190, 'teleporter': [16, 6], 'access': []}, {'target_room': 75, 'entrance': 191, 'teleporter': [63, 8], 'access': ['GeminiCrest']}]}, {'name': 'Volcano Base', 'id': 93, 'game_objects': [{'name': 'Volcano - Base Chest', 'object_id': 17, 'type': 'Chest', 'access': []}, {'name': 'Volcano - Base West Box', 'object_id': 127, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Left Box', 'object_id': 128, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Right Box', 'object_id': 129, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 192, 'teleporter': [103, 0], 'access': []}, {'target_room': 98, 'entrance': 196, 'teleporter': [31, 8], 'access': []}, {'target_room': 96, 'entrance': 197, 'teleporter': [30, 8], 'access': []}]}, {'name': 'Volcano Top Left', 'id': 94, 'game_objects': [{'name': 'Volcano - Medusa Chest', 'object_id': 18, 'type': 'Chest', 'access': ['Medusa']}, {'name': 'Medusa', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Medusa'], 'access': []}, {'name': 'Volcano - Behind Medusa Box', 'object_id': 130, 'type': 'Box', 'access': []}], 'links': [{'target_room': 209, 'entrance': 199, 'teleporter': [26, 8], 'access': []}]}, {'name': 'Volcano Top Right', 'id': 95, 'game_objects': [{'name': 'Volcano - Top of the Volcano Left Box', 'object_id': 131, 'type': 'Box', 'access': []}, {'name': 'Volcano - Top of the Volcano Right Box', 'object_id': 132, 'type': 'Box', 'access': []}], 'links': [{'target_room': 99, 'entrance': 200, 'teleporter': [79, 8], 'access': []}]}, {'name': 'Volcano Right Path', 'id': 96, 'game_objects': [{'name': 'Volcano - Right Path Box', 'object_id': 135, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 201, 'teleporter': [15, 8], 'access': []}]}, {'name': 'Volcano Left Path', 'id': 98, 'game_objects': [{'name': 'Volcano - Left Path Box', 'object_id': 134, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 204, 'teleporter': [27, 8], 'access': []}, {'target_room': 99, 'entrance': 202, 'teleporter': [25, 2], 'access': []}, {'target_room': 209, 'entrance': 203, 'teleporter': [26, 2], 'access': []}]}, {'name': 'Volcano Cross Left-Right', 'id': 99, 'game_objects': [], 'links': [{'target_room': 95, 'entrance': 206, 'teleporter': [29, 8], 'access': []}, {'target_room': 98, 'entrance': 205, 'teleporter': [103, 3], 'access': []}]}, {'name': 'Volcano Cross Right-Left', 'id': 209, 'game_objects': [{'name': 'Volcano - Crossover Section Box', 'object_id': 133, 'type': 'Box', 'access': []}], 'links': [{'target_room': 98, 'entrance': 208, 'teleporter': [104, 3], 'access': []}, {'target_room': 94, 'entrance': 207, 'teleporter': [28, 8], 'access': []}]}, {'name': 'Lava Dome Inner Ring Main Loop', 'id': 100, 'game_objects': [{'name': 'Lava Dome - Exterior Caldera Near Switch Cliff Box', 'object_id': 136, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Exterior South Cliff Box', 'object_id': 137, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 209, 'teleporter': [104, 0], 'access': []}, {'target_room': 113, 'entrance': 211, 'teleporter': [105, 0], 'access': []}, {'target_room': 114, 'entrance': 212, 'teleporter': [106, 0], 'access': []}, {'target_room': 116, 'entrance': 213, 'teleporter': [108, 0], 'access': []}, {'target_room': 118, 'entrance': 214, 'teleporter': [111, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Center Ledge', 'id': 101, 'game_objects': [{'name': 'Lava Dome - Exterior Center Dropoff Ledge Box', 'object_id': 138, 'type': 'Box', 'access': []}], 'links': [{'target_room': 115, 'entrance': 215, 'teleporter': [107, 0], 'access': []}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Plate Ledge', 'id': 102, 'game_objects': [{'name': 'Lava Dome Plate', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LavaDomePlate'], 'access': []}], 'links': [{'target_room': 119, 'entrance': 216, 'teleporter': [109, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Upper Ledge West', 'id': 103, 'game_objects': [], 'links': [{'target_room': 111, 'entrance': 219, 'teleporter': [112, 0], 'access': []}, {'target_room': 108, 'entrance': 220, 'teleporter': [113, 0], 'access': []}, {'target_room': 104, 'access': ['Claw']}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Upper Ledge East', 'id': 104, 'game_objects': [], 'links': [{'target_room': 110, 'entrance': 218, 'teleporter': [110, 0], 'access': []}, {'target_room': 103, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Big Door Ledge', 'id': 105, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 221, 'teleporter': [114, 0], 'access': []}, {'target_room': 121, 'entrance': 222, 'teleporter': [29, 2], 'access': ['LavaDomePlate']}]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge', 'id': 106, 'game_objects': [{'name': 'Lava Dome - Exterior Dead End Caldera Box', 'object_id': 139, 'type': 'Box', 'access': []}], 'links': [{'target_room': 120, 'entrance': 226, 'teleporter': [115, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze II', 'id': 107, 'game_objects': [{'name': 'Lava Dome - Gold Maze Northwest Box', 'object_id': 140, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southwest Box', 'object_id': 246, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Northeast Box', 'object_id': 247, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze North Box', 'object_id': 248, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Center Box', 'object_id': 249, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southeast Box', 'object_id': 250, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 227, 'teleporter': [116, 0], 'access': []}, {'target_room': 108, 'entrance': 228, 'teleporter': [119, 0], 'access': []}, {'target_room': 120, 'entrance': 229, 'teleporter': [120, 0], 'access': []}]}, {'name': 'Lava Dome Up-Down Corridor', 'id': 108, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 231, 'teleporter': [118, 0], 'access': []}, {'target_room': 103, 'entrance': 230, 'teleporter': [117, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze I', 'id': 109, 'game_objects': [{'name': 'Lava Dome - Bare Maze Leapfrog Alcove North Box', 'object_id': 141, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Leapfrog Alcove South Box', 'object_id': 142, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Center Box', 'object_id': 143, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Southwest Box', 'object_id': 144, 'type': 'Box', 'access': []}], 'links': [{'target_room': 118, 'entrance': 232, 'teleporter': [121, 0], 'access': []}, {'target_room': 111, 'entrance': 233, 'teleporter': [122, 0], 'access': []}]}, {'name': 'Lava Dome Pointless Room', 'id': 110, 'game_objects': [], 'links': [{'target_room': 104, 'entrance': 234, 'teleporter': [123, 0], 'access': []}]}, {'name': 'Lava Dome Lower Moon Helm Room', 'id': 111, 'game_objects': [{'name': 'Lava Dome - U-Bend Room North Box', 'object_id': 146, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - U-Bend Room South Box', 'object_id': 147, 'type': 'Box', 'access': []}], 'links': [{'target_room': 103, 'entrance': 235, 'teleporter': [124, 0], 'access': []}, {'target_room': 109, 'entrance': 236, 'teleporter': [125, 0], 'access': []}]}, {'name': 'Lava Dome Moon Helm Room', 'id': 112, 'game_objects': [{'name': 'Lava Dome - Beyond River Room Chest', 'object_id': 19, 'type': 'Chest', 'access': []}, {'name': 'Lava Dome - Beyond River Room Box', 'object_id': 145, 'type': 'Box', 'access': []}], 'links': [{'target_room': 117, 'entrance': 237, 'teleporter': [126, 0], 'access': []}]}, {'name': 'Lava Dome Three Jumps Room', 'id': 113, 'game_objects': [{'name': 'Lava Dome - Three Jumps Room Box', 'object_id': 150, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 238, 'teleporter': [127, 0], 'access': []}]}, {'name': 'Lava Dome Life Chest Room Lower Ledge', 'id': 114, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Boulder Chest', 'object_id': 28, 'type': 'Chest', 'access': ['MegaGrenade']}], 'links': [{'target_room': 100, 'entrance': 239, 'teleporter': [128, 0], 'access': []}, {'target_room': 115, 'access': ['Claw']}]}, {'name': 'Lava Dome Life Chest Room Upper Ledge', 'id': 115, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box West', 'object_id': 148, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box East', 'object_id': 149, 'type': 'Box', 'access': []}], 'links': [{'target_room': 101, 'entrance': 240, 'teleporter': [129, 0], 'access': []}, {'target_room': 114, 'access': ['Claw']}]}, {'name': 'Lava Dome Big Jump Room Main Area', 'id': 116, 'game_objects': [{'name': 'Lava Dome - Lava River Room North Box', 'object_id': 152, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room East Box', 'object_id': 153, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room South Box', 'object_id': 154, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 241, 'teleporter': [133, 0], 'access': []}, {'target_room': 119, 'entrance': 243, 'teleporter': [132, 0], 'access': []}, {'target_room': 117, 'access': ['MegaGrenade']}]}, {'name': 'Lava Dome Big Jump Room MegaGrenade Area', 'id': 117, 'game_objects': [], 'links': [{'target_room': 112, 'entrance': 242, 'teleporter': [131, 0], 'access': []}, {'target_room': 116, 'access': ['Bomb']}]}, {'name': 'Lava Dome Split Corridor', 'id': 118, 'game_objects': [{'name': 'Lava Dome - Split Corridor Box', 'object_id': 151, 'type': 'Box', 'access': []}], 'links': [{'target_room': 109, 'entrance': 244, 'teleporter': [130, 0], 'access': []}, {'target_room': 100, 'entrance': 245, 'teleporter': [134, 0], 'access': []}]}, {'name': 'Lava Dome Plate Corridor', 'id': 119, 'game_objects': [], 'links': [{'target_room': 102, 'entrance': 246, 'teleporter': [135, 0], 'access': []}, {'target_room': 116, 'entrance': 247, 'teleporter': [137, 0], 'access': []}]}, {'name': 'Lava Dome Four Boxes Stairs', 'id': 120, 'game_objects': [{'name': 'Lava Dome - Caldera Stairway West Left Box', 'object_id': 155, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway West Right Box', 'object_id': 156, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Left Box', 'object_id': 157, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Right Box', 'object_id': 158, 'type': 'Box', 'access': []}], 'links': [{'target_room': 107, 'entrance': 248, 'teleporter': [136, 0], 'access': []}, {'target_room': 106, 'entrance': 249, 'teleporter': [16, 0], 'access': []}]}, {'name': 'Lava Dome Hydra Room', 'id': 121, 'game_objects': [{'name': 'Lava Dome - Dualhead Hydra Chest', 'object_id': 20, 'type': 'Chest', 'access': ['DualheadHydra']}, {'name': 'Dualhead Hydra', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['DualheadHydra'], 'access': []}, {'name': 'Lava Dome - Hydra Room Northwest Box', 'object_id': 159, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Hydra Room Southweast Box', 'object_id': 160, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 250, 'teleporter': [105, 3], 'access': []}, {'target_room': 122, 'entrance': 251, 'teleporter': [138, 0], 'access': ['DualheadHydra']}]}, {'name': 'Lava Dome Escape Corridor', 'id': 122, 'game_objects': [], 'links': [{'target_room': 121, 'entrance': 253, 'teleporter': [139, 0], 'access': []}]}, {'name': 'Rope Bridge', 'id': 123, 'game_objects': [{'name': 'Rope Bridge - West Box', 'object_id': 163, 'type': 'Box', 'access': []}, {'name': 'Rope Bridge - East Box', 'object_id': 164, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 255, 'teleporter': [140, 0], 'access': []}]}, {'name': 'Alive Forest', 'id': 124, 'game_objects': [{'name': 'Alive Forest - Tree Stump Chest', 'object_id': 21, 'type': 'Chest', 'access': ['Axe']}, {'name': 'Alive Forest - Near Entrance Box', 'object_id': 165, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - After Bridge Box', 'object_id': 166, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - Gemini Stump Box', 'object_id': 167, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 226, 'entrance': 272, 'teleporter': [142, 0], 'access': ['Axe']}, {'target_room': 21, 'entrance': 275, 'teleporter': [64, 8], 'access': ['LibraCrest', 'Axe']}, {'target_room': 22, 'entrance': 276, 'teleporter': [65, 8], 'access': ['GeminiCrest', 'Axe']}, {'target_room': 23, 'entrance': 277, 'teleporter': [66, 8], 'access': ['MobiusCrest', 'Axe']}, {'target_room': 125, 'entrance': 274, 'teleporter': [143, 0], 'access': ['Axe']}]}, {'name': 'Giant Tree 1F Main Area', 'id': 125, 'game_objects': [{'name': 'Giant Tree 1F - Northwest Box', 'object_id': 168, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Southwest Box', 'object_id': 169, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Center Box', 'object_id': 170, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - East Box', 'object_id': 171, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 278, 'teleporter': [56, 1], 'access': []}, {'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F North Island', 'id': 202, 'game_objects': [], 'links': [{'target_room': 127, 'entrance': 280, 'teleporter': [144, 0], 'access': []}, {'target_room': 125, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F Central Island', 'id': 126, 'game_objects': [], 'links': [{'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Main Lobby', 'id': 127, 'game_objects': [{'name': 'Giant Tree 2F - North Box', 'object_id': 172, 'type': 'Box', 'access': []}], 'links': [{'target_room': 126, 'access': ['DragonClaw']}, {'target_room': 125, 'entrance': 281, 'teleporter': [145, 0], 'access': []}, {'target_room': 133, 'entrance': 283, 'teleporter': [149, 0], 'access': []}, {'target_room': 129, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F West Ledge', 'id': 128, 'game_objects': [{'name': 'Giant Tree 2F - Dropdown Ledge Box', 'object_id': 174, 'type': 'Box', 'access': []}], 'links': [{'target_room': 140, 'entrance': 284, 'teleporter': [147, 0], 'access': ['Sword']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Lower Area', 'id': 129, 'game_objects': [{'name': 'Giant Tree 2F - South Box', 'object_id': 173, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'access': ['Claw']}, {'target_room': 131, 'access': ['Claw']}]}, {'name': 'Giant Tree 2F Central Island', 'id': 130, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 135, 'entrance': 282, 'teleporter': [146, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 2F East Ledge', 'id': 131, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Meteor Chest Room', 'id': 132, 'game_objects': [{'name': 'Giant Tree 2F - Gidrah Chest', 'object_id': 22, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 133, 'entrance': 285, 'teleporter': [148, 0], 'access': []}]}, {'name': 'Giant Tree 2F Mushroom Room', 'id': 133, 'game_objects': [{'name': 'Giant Tree 2F - Mushroom Tunnel West Box', 'object_id': 175, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 2F - Mushroom Tunnel East Box', 'object_id': 176, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 127, 'entrance': 286, 'teleporter': [150, 0], 'access': ['Axe']}, {'target_room': 132, 'entrance': 287, 'teleporter': [151, 0], 'access': ['Axe', 'Gidrah']}]}, {'name': 'Giant Tree 3F Central Island', 'id': 135, 'game_objects': [{'name': 'Giant Tree 3F - Central Island Box', 'object_id': 179, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'entrance': 288, 'teleporter': [152, 0], 'access': []}, {'target_room': 136, 'access': ['Claw']}, {'target_room': 137, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 3F Central Area', 'id': 136, 'game_objects': [{'name': 'Giant Tree 3F - Center North Box', 'object_id': 177, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 3F - Center West Box', 'object_id': 178, 'type': 'Box', 'access': []}], 'links': [{'target_room': 135, 'access': ['Claw']}, {'target_room': 127, 'access': []}, {'target_room': 131, 'access': []}]}, {'name': 'Giant Tree 3F Lower Ledge', 'id': 137, 'game_objects': [], 'links': [{'target_room': 135, 'access': ['DragonClaw']}, {'target_room': 142, 'entrance': 289, 'teleporter': [153, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 3F West Area', 'id': 138, 'game_objects': [{'name': 'Giant Tree 3F - West Side Box', 'object_id': 180, 'type': 'Box', 'access': []}], 'links': [{'target_room': 128, 'access': []}, {'target_room': 210, 'entrance': 290, 'teleporter': [154, 0], 'access': []}]}, {'name': 'Giant Tree 3F Middle Up Island', 'id': 139, 'game_objects': [], 'links': [{'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree 3F West Platform', 'id': 140, 'game_objects': [], 'links': [{'target_room': 139, 'access': ['Claw']}, {'target_room': 141, 'access': ['Claw']}, {'target_room': 128, 'entrance': 291, 'teleporter': [155, 0], 'access': []}]}, {'name': 'Giant Tree 3F North Ledge', 'id': 141, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 292, 'teleporter': [156, 0], 'access': ['Sword']}, {'target_room': 139, 'access': ['Claw']}, {'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree Worm Room Upper Ledge', 'id': 142, 'game_objects': [{'name': 'Giant Tree 3F - Worm Room North Box', 'object_id': 181, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 3F - Worm Room South Box', 'object_id': 182, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 137, 'entrance': 293, 'teleporter': [157, 0], 'access': ['Axe']}, {'target_room': 210, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree Worm Room Lower Ledge', 'id': 210, 'game_objects': [], 'links': [{'target_room': 138, 'entrance': 294, 'teleporter': [158, 0], 'access': []}]}, {'name': 'Giant Tree 4F Lower Floor', 'id': 143, 'game_objects': [], 'links': [{'target_room': 141, 'entrance': 295, 'teleporter': [159, 0], 'access': []}, {'target_room': 148, 'entrance': 296, 'teleporter': [160, 0], 'access': []}, {'target_room': 148, 'entrance': 297, 'teleporter': [161, 0], 'access': []}, {'target_room': 147, 'entrance': 298, 'teleporter': [162, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 4F Middle Floor', 'id': 144, 'game_objects': [{'name': 'Giant Tree 4F - Highest Platform North Box', 'object_id': 183, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Highest Platform South Box', 'object_id': 184, 'type': 'Box', 'access': []}], 'links': [{'target_room': 149, 'entrance': 299, 'teleporter': [163, 0], 'access': []}, {'target_room': 145, 'access': ['Claw']}, {'target_room': 146, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Upper Floor', 'id': 145, 'game_objects': [], 'links': [{'target_room': 150, 'entrance': 300, 'teleporter': [164, 0], 'access': ['Sword']}, {'target_room': 144, 'access': ['Claw']}]}, {'name': 'Giant Tree 4F South Ledge', 'id': 146, 'game_objects': [{'name': 'Giant Tree 4F - Hook Ledge Northeast Box', 'object_id': 185, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Hook Ledge Southwest Box', 'object_id': 186, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Slime Room East Area', 'id': 147, 'game_objects': [{'name': 'Giant Tree 4F - East Slime Room Box', 'object_id': 188, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 143, 'entrance': 304, 'teleporter': [168, 0], 'access': []}]}, {'name': 'Giant Tree 4F Slime Room West Area', 'id': 148, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 303, 'teleporter': [167, 0], 'access': ['Axe']}, {'target_room': 143, 'entrance': 302, 'teleporter': [166, 0], 'access': ['Axe']}, {'target_room': 149, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree 4F Slime Room Platform', 'id': 149, 'game_objects': [{'name': 'Giant Tree 4F - West Slime Room Box', 'object_id': 187, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'entrance': 301, 'teleporter': [165, 0], 'access': []}, {'target_room': 148, 'access': ['Claw']}]}, {'name': 'Giant Tree 5F Lower Area', 'id': 150, 'game_objects': [{'name': 'Giant Tree 5F - Northwest Left Box', 'object_id': 189, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - Northwest Right Box', 'object_id': 190, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Left Box', 'object_id': 191, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Right Box', 'object_id': 192, 'type': 'Box', 'access': []}], 'links': [{'target_room': 145, 'entrance': 305, 'teleporter': [169, 0], 'access': []}, {'target_room': 151, 'access': ['Claw']}, {'target_room': 143, 'access': []}]}, {'name': 'Giant Tree 5F Gidrah Platform', 'id': 151, 'game_objects': [{'name': 'Gidrah', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Gidrah'], 'access': []}], 'links': [{'target_room': 150, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Lower Ledge', 'id': 152, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 307, 'teleporter': [18, 6], 'access': []}, {'target_room': 153, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Upper Ledge', 'id': 153, 'game_objects': [{'name': 'Kaidge Temple - Box', 'object_id': 193, 'type': 'Box', 'access': []}], 'links': [{'target_room': 185, 'entrance': 308, 'teleporter': [71, 8], 'access': ['MobiusCrest']}, {'target_room': 152, 'access': ['Claw']}]}, {'name': 'Windhole Temple', 'id': 154, 'game_objects': [{'name': 'Windhole Temple - Box', 'object_id': 194, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 309, 'teleporter': [173, 0], 'access': []}]}, {'name': 'Mount Gale', 'id': 155, 'game_objects': [{'name': 'Mount Gale - Dullahan Chest', 'object_id': 23, 'type': 'Chest', 'access': ['DragonClaw', 'Dullahan']}, {'name': 'Dullahan', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Dullahan'], 'access': ['DragonClaw']}, {'name': 'Mount Gale - East Box', 'object_id': 195, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Mount Gale - West Box', 'object_id': 196, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 310, 'teleporter': [174, 0], 'access': []}]}, {'name': 'Windia', 'id': 156, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 312, 'teleporter': [10, 6], 'access': []}, {'target_room': 157, 'entrance': 320, 'teleporter': [30, 5], 'access': []}, {'target_room': 163, 'entrance': 321, 'teleporter': [97, 8], 'access': []}, {'target_room': 165, 'entrance': 322, 'teleporter': [32, 5], 'access': []}, {'target_room': 159, 'entrance': 323, 'teleporter': [176, 4], 'access': []}, {'target_room': 160, 'entrance': 324, 'teleporter': [177, 4], 'access': []}]}, {'name': "Otto's House", 'id': 157, 'game_objects': [{'name': 'Otto', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['RainbowBridge'], 'access': ['ThunderRock']}], 'links': [{'target_room': 156, 'entrance': 327, 'teleporter': [106, 3], 'access': []}, {'target_room': 158, 'entrance': 326, 'teleporter': [33, 2], 'access': []}]}, {'name': "Otto's Attic", 'id': 158, 'game_objects': [{'name': "Windia - Otto's Attic Box", 'object_id': 197, 'type': 'Box', 'access': []}], 'links': [{'target_room': 157, 'entrance': 328, 'teleporter': [107, 3], 'access': []}]}, {'name': 'Windia Kid House', 'id': 159, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 329, 'teleporter': [178, 0], 'access': []}, {'target_room': 161, 'entrance': 330, 'teleporter': [180, 0], 'access': []}]}, {'name': 'Windia Old People House', 'id': 160, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 331, 'teleporter': [179, 0], 'access': []}, {'target_room': 162, 'entrance': 332, 'teleporter': [181, 0], 'access': []}]}, {'name': 'Windia Kid House Basement', 'id': 161, 'game_objects': [], 'links': [{'target_room': 159, 'entrance': 333, 'teleporter': [182, 0], 'access': []}, {'target_room': 79, 'entrance': 334, 'teleporter': [44, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Old People House Basement', 'id': 162, 'game_objects': [{'name': 'Windia - Mobius Basement West Box', 'object_id': 200, 'type': 'Box', 'access': []}, {'name': 'Windia - Mobius Basement East Box', 'object_id': 201, 'type': 'Box', 'access': []}], 'links': [{'target_room': 160, 'entrance': 335, 'teleporter': [183, 0], 'access': []}, {'target_room': 186, 'entrance': 336, 'teleporter': [43, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Inn Lobby', 'id': 163, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 338, 'teleporter': [135, 3], 'access': []}, {'target_room': 164, 'entrance': 337, 'teleporter': [102, 8], 'access': []}]}, {'name': 'Windia Inn Beds', 'id': 164, 'game_objects': [{'name': 'Windia - Inn Bedroom North Box', 'object_id': 198, 'type': 'Box', 'access': []}, {'name': 'Windia - Inn Bedroom South Box', 'object_id': 199, 'type': 'Box', 'access': []}, {'name': 'Windia - Kaeli', 'object_id': 15, 'type': 'NPC', 'access': ['Kaeli2']}], 'links': [{'target_room': 163, 'entrance': 339, 'teleporter': [216, 0], 'access': []}]}, {'name': 'Windia Vendor House', 'id': 165, 'game_objects': [{'name': 'Windia - Vendor', 'object_id': 16, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 156, 'entrance': 340, 'teleporter': [108, 3], 'access': []}]}, {'name': 'Pazuzu Tower 1F Main Lobby', 'id': 166, 'game_objects': [{'name': 'Pazuzu 1F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu1F'], 'access': []}], 'links': [{'target_room': 226, 'entrance': 341, 'teleporter': [184, 0], 'access': []}, {'target_room': 180, 'entrance': 345, 'teleporter': [185, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Boxes Room', 'id': 167, 'game_objects': [{'name': "Pazuzu's Tower 1F - Descent Bomb Wall West Box", 'object_id': 202, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall Center Box", 'object_id': 203, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall East Box", 'object_id': 204, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Box", 'object_id': 205, 'type': 'Box', 'access': []}], 'links': [{'target_room': 169, 'entrance': 349, 'teleporter': [187, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Southern Platform', 'id': 168, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 346, 'teleporter': [186, 0], 'access': []}, {'target_room': 166, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 2F', 'id': 169, 'game_objects': [{'name': "Pazuzu's Tower 2F - East Room West Box", 'object_id': 206, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 2F - East Room East Box", 'object_id': 207, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 2F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 2F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 350, 'teleporter': [188, 0], 'access': []}, {'target_room': 168, 'entrance': 351, 'teleporter': [189, 0], 'access': []}, {'target_room': 167, 'entrance': 352, 'teleporter': [190, 0], 'access': []}, {'target_room': 171, 'entrance': 353, 'teleporter': [191, 0], 'access': []}]}, {'name': 'Pazuzu 3F Main Room', 'id': 170, 'game_objects': [{'name': "Pazuzu's Tower 3F - Guest Room West Box", 'object_id': 208, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 3F - Guest Room East Box", 'object_id': 209, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 3F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu3F'], 'access': []}], 'links': [{'target_room': 180, 'entrance': 356, 'teleporter': [192, 0], 'access': []}, {'target_room': 181, 'entrance': 357, 'teleporter': [193, 0], 'access': []}]}, {'name': 'Pazuzu 3F Central Island', 'id': 171, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 360, 'teleporter': [194, 0], 'access': []}, {'target_room': 170, 'access': ['DragonClaw']}, {'target_room': 172, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 3F Southern Island', 'id': 172, 'game_objects': [{'name': "Pazuzu's Tower 3F - South Ledge Box", 'object_id': 210, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 361, 'teleporter': [195, 0], 'access': []}, {'target_room': 171, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 4F', 'id': 173, 'game_objects': [{'name': "Pazuzu's Tower 4F - Elevator West Box", 'object_id': 211, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - Elevator East Box", 'object_id': 212, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - East Storage Room Chest", 'object_id': 24, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 4F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 4F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 362, 'teleporter': [196, 0], 'access': []}, {'target_room': 184, 'entrance': 363, 'teleporter': [197, 0], 'access': []}, {'target_room': 172, 'entrance': 364, 'teleporter': [198, 0], 'access': []}, {'target_room': 175, 'entrance': 365, 'teleporter': [199, 0], 'access': []}]}, {'name': 'Pazuzu 5F Pazuzu Loop', 'id': 174, 'game_objects': [{'name': 'Pazuzu 5F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu5F'], 'access': []}], 'links': [{'target_room': 181, 'entrance': 368, 'teleporter': [200, 0], 'access': []}, {'target_room': 182, 'entrance': 369, 'teleporter': [201, 0], 'access': []}]}, {'name': 'Pazuzu 5F Upper Loop', 'id': 175, 'game_objects': [{'name': "Pazuzu's Tower 5F - North Box", 'object_id': 213, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 5F - South Box", 'object_id': 214, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 370, 'teleporter': [202, 0], 'access': []}, {'target_room': 176, 'entrance': 371, 'teleporter': [203, 0], 'access': []}]}, {'name': 'Pazuzu 6F', 'id': 176, 'game_objects': [{'name': "Pazuzu's Tower 6F - Box", 'object_id': 215, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 6F - Chest", 'object_id': 25, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 6F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6FLock'], 'access': ['Bomb', 'Axe']}, {'name': 'Pazuzu 6F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6F'], 'access': ['Bomb']}], 'links': [{'target_room': 184, 'entrance': 374, 'teleporter': [204, 0], 'access': []}, {'target_room': 175, 'entrance': 375, 'teleporter': [205, 0], 'access': []}, {'target_room': 178, 'entrance': 376, 'teleporter': [206, 0], 'access': []}, {'target_room': 178, 'entrance': 377, 'teleporter': [207, 0], 'access': []}]}, {'name': 'Pazuzu 7F Southwest Area', 'id': 177, 'game_objects': [], 'links': [{'target_room': 182, 'entrance': 380, 'teleporter': [26, 0], 'access': []}, {'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 7F Rest of the Area', 'id': 178, 'game_objects': [], 'links': [{'target_room': 177, 'access': ['DragonClaw']}, {'target_room': 176, 'entrance': 381, 'teleporter': [27, 0], 'access': []}, {'target_room': 176, 'entrance': 382, 'teleporter': [28, 0], 'access': []}, {'target_room': 179, 'access': ['DragonClaw', 'Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}]}, {'name': 'Pazuzu 7F Sky Room', 'id': 179, 'game_objects': [{'name': "Pazuzu's Tower 7F - Pazuzu Chest", 'object_id': 26, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu'], 'access': ['Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}], 'links': [{'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 1F to 3F', 'id': 180, 'game_objects': [], 'links': [{'target_room': 166, 'entrance': 385, 'teleporter': [29, 0], 'access': []}, {'target_room': 170, 'entrance': 386, 'teleporter': [30, 0], 'access': []}]}, {'name': 'Pazuzu 3F to 5F', 'id': 181, 'game_objects': [], 'links': [{'target_room': 170, 'entrance': 387, 'teleporter': [40, 0], 'access': []}, {'target_room': 174, 'entrance': 388, 'teleporter': [41, 0], 'access': []}]}, {'name': 'Pazuzu 5F to 7F', 'id': 182, 'game_objects': [], 'links': [{'target_room': 174, 'entrance': 389, 'teleporter': [38, 0], 'access': []}, {'target_room': 177, 'entrance': 390, 'teleporter': [39, 0], 'access': []}]}, {'name': 'Pazuzu 2F to 4F', 'id': 183, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 391, 'teleporter': [21, 0], 'access': []}, {'target_room': 173, 'entrance': 392, 'teleporter': [22, 0], 'access': []}]}, {'name': 'Pazuzu 4F to 6F', 'id': 184, 'game_objects': [], 'links': [{'target_room': 173, 'entrance': 393, 'teleporter': [2, 0], 'access': []}, {'target_room': 176, 'entrance': 394, 'teleporter': [3, 0], 'access': []}]}, {'name': 'Light Temple', 'id': 185, 'game_objects': [{'name': 'Light Temple - Box', 'object_id': 216, 'type': 'Box', 'access': []}], 'links': [{'target_room': 230, 'entrance': 395, 'teleporter': [19, 6], 'access': []}, {'target_room': 153, 'entrance': 396, 'teleporter': [70, 8], 'access': ['MobiusCrest']}]}, {'name': 'Ship Dock', 'id': 186, 'game_objects': [{'name': 'Ship Dock Access', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipDockAccess'], 'access': []}], 'links': [{'target_room': 228, 'entrance': 399, 'teleporter': [17, 6], 'access': []}, {'target_room': 162, 'entrance': 397, 'teleporter': [61, 8], 'access': ['MobiusCrest']}]}, {'name': 'Mac Ship Deck', 'id': 187, 'game_objects': [{'name': 'Mac Ship Steering Wheel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipSteeringWheel'], 'access': []}, {'name': "Mac's Ship Deck - North Box", 'object_id': 217, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - Center Box", 'object_id': 218, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - South Box", 'object_id': 219, 'type': 'Box', 'access': []}], 'links': [{'target_room': 229, 'entrance': 400, 'teleporter': [37, 8], 'access': []}, {'target_room': 188, 'entrance': 401, 'teleporter': [50, 8], 'access': []}, {'target_room': 188, 'entrance': 402, 'teleporter': [51, 8], 'access': []}, {'target_room': 188, 'entrance': 403, 'teleporter': [52, 8], 'access': []}, {'target_room': 189, 'entrance': 404, 'teleporter': [53, 8], 'access': []}]}, {'name': 'Mac Ship B1 Outer Ring', 'id': 188, 'game_objects': [{'name': "Mac's Ship B1 - Northwest Hook Platform Box", 'object_id': 228, 'type': 'Box', 'access': ['DragonClaw']}, {'name': "Mac's Ship B1 - Center Hook Platform Box", 'object_id': 229, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 187, 'entrance': 405, 'teleporter': [208, 0], 'access': []}, {'target_room': 187, 'entrance': 406, 'teleporter': [175, 0], 'access': []}, {'target_room': 187, 'entrance': 407, 'teleporter': [172, 0], 'access': []}, {'target_room': 193, 'entrance': 408, 'teleporter': [88, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B1 Square Room', 'id': 189, 'game_objects': [], 'links': [{'target_room': 187, 'entrance': 409, 'teleporter': [141, 0], 'access': []}, {'target_room': 192, 'entrance': 410, 'teleporter': [87, 0], 'access': []}]}, {'name': 'Mac Ship B1 Central Corridor', 'id': 190, 'game_objects': [{'name': "Mac's Ship B1 - Central Corridor Box", 'object_id': 230, 'type': 'Box', 'access': []}], 'links': [{'target_room': 192, 'entrance': 413, 'teleporter': [86, 0], 'access': []}, {'target_room': 191, 'entrance': 412, 'teleporter': [102, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B2 South Corridor', 'id': 191, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 415, 'teleporter': [55, 8], 'access': []}, {'target_room': 194, 'entrance': 414, 'teleporter': [57, 1], 'access': []}]}, {'name': 'Mac Ship B2 North Corridor', 'id': 192, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 416, 'teleporter': [56, 8], 'access': []}, {'target_room': 189, 'entrance': 417, 'teleporter': [57, 8], 'access': []}]}, {'name': 'Mac Ship B2 Outer Ring', 'id': 193, 'game_objects': [{'name': "Mac's Ship B2 - Barrel Room South Box", 'object_id': 223, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Barrel Room North Box", 'object_id': 224, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southwest Room Box", 'object_id': 225, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southeast Room Box", 'object_id': 226, 'type': 'Box', 'access': []}], 'links': [{'target_room': 188, 'entrance': 418, 'teleporter': [58, 8], 'access': []}]}, {'name': 'Mac Ship B1 Mac Room', 'id': 194, 'game_objects': [{'name': "Mac's Ship B1 - Mac Room Chest", 'object_id': 27, 'type': 'Chest', 'access': []}, {'name': 'Captain Mac', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLoaned'], 'access': ['CaptainCap']}], 'links': [{'target_room': 191, 'entrance': 424, 'teleporter': [101, 0], 'access': []}]}, {'name': 'Doom Castle Corridor of Destiny', 'id': 195, 'game_objects': [], 'links': [{'target_room': 201, 'entrance': 428, 'teleporter': [84, 0], 'access': []}, {'target_room': 196, 'entrance': 429, 'teleporter': [35, 2], 'access': []}, {'target_room': 197, 'entrance': 430, 'teleporter': [209, 0], 'access': ['StoneGolem']}, {'target_room': 198, 'entrance': 431, 'teleporter': [211, 0], 'access': ['StoneGolem', 'TwinheadWyvern']}, {'target_room': 199, 'entrance': 432, 'teleporter': [13, 2], 'access': ['StoneGolem', 'TwinheadWyvern', 'Zuh']}]}, {'name': 'Doom Castle Ice Floor', 'id': 196, 'game_objects': [{'name': 'Doom Castle 4F - Northwest Room Box', 'object_id': 231, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Southwest Room Box', 'object_id': 232, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Northeast Room Box', 'object_id': 233, 'type': 'Box', 'access': ['Sword']}, {'name': 'Doom Castle 4F - Southeast Room Box', 'object_id': 234, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Stone Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['StoneGolem'], 'access': ['Sword', 'DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 433, 'teleporter': [109, 3], 'access': []}]}, {'name': 'Doom Castle Lava Floor', 'id': 197, 'game_objects': [{'name': 'Doom Castle 5F - North Left Box', 'object_id': 235, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - North Right Box', 'object_id': 236, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Left Box', 'object_id': 237, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Right Box', 'object_id': 238, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Twinhead Wyvern', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TwinheadWyvern'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 434, 'teleporter': [210, 0], 'access': []}]}, {'name': 'Doom Castle Sky Floor', 'id': 198, 'game_objects': [{'name': 'Doom Castle 6F - West Box', 'object_id': 239, 'type': 'Box', 'access': []}, {'name': 'Doom Castle 6F - East Box', 'object_id': 240, 'type': 'Box', 'access': []}, {'name': 'Zuh', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Zuh'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 435, 'teleporter': [212, 0], 'access': []}, {'target_room': 197, 'access': []}]}, {'name': 'Doom Castle Hero Room', 'id': 199, 'game_objects': [{'name': 'Doom Castle Hero Chest 01', 'object_id': 242, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 02', 'object_id': 243, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 03', 'object_id': 244, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 04', 'object_id': 245, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 200, 'entrance': 436, 'teleporter': [54, 0], 'access': []}, {'target_room': 195, 'entrance': 441, 'teleporter': [110, 3], 'access': []}]}, {'name': 'Doom Castle Dark King Room', 'id': 200, 'game_objects': [], 'links': [{'target_room': 199, 'entrance': 442, 'teleporter': [52, 0], 'access': []}]}] +entrances = [{'name': 'Doom Castle - Sand Floor - To Sky Door - Sand Floor', 'id': 0, 'area': 7, 'coordinates': [24, 19], 'teleporter': [0, 0]}, {'name': 'Doom Castle - Sand Floor - Main Entrance - Sand Floor', 'id': 1, 'area': 7, 'coordinates': [19, 43], 'teleporter': [1, 6]}, {'name': 'Doom Castle - Aero Room - Aero Room Entrance', 'id': 2, 'area': 7, 'coordinates': [27, 39], 'teleporter': [1, 0]}, {'name': 'Focus Tower B1 - Main Loop - South Entrance', 'id': 3, 'area': 8, 'coordinates': [43, 60], 'teleporter': [2, 6]}, {'name': 'Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall', 'id': 4, 'area': 8, 'coordinates': [37, 41], 'teleporter': [4, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room', 'id': 5, 'area': 8, 'coordinates': [59, 35], 'teleporter': [5, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest', 'id': 6, 'area': 8, 'coordinates': [57, 59], 'teleporter': [8, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door', 'id': 7, 'area': 8, 'coordinates': [51, 49], 'teleporter': [6, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor', 'id': 8, 'area': 8, 'coordinates': [51, 45], 'teleporter': [7, 0]}, {'name': 'Focus Tower 1F - Focus Tower West Entrance', 'id': 9, 'area': 9, 'coordinates': [25, 29], 'teleporter': [3, 6]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From SandCoin', 'id': 10, 'area': 9, 'coordinates': [16, 4], 'teleporter': [10, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - Main Hall', 'id': 11, 'area': 9, 'coordinates': [4, 23], 'teleporter': [11, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - To Aero Chest', 'id': 12, 'area': 9, 'coordinates': [26, 17], 'teleporter': [12, 0]}, {'name': 'Focus Tower 1F - Sky Door', 'id': 13, 'area': 9, 'coordinates': [16, 24], 'teleporter': [13, 0]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From RiverCoin', 'id': 14, 'area': 9, 'coordinates': [16, 10], 'teleporter': [14, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - From Sky Door', 'id': 15, 'area': 9, 'coordinates': [16, 29], 'teleporter': [15, 0]}, {'name': 'Focus Tower 2F - Sand Coin Passage - North Entrance', 'id': 16, 'area': 10, 'coordinates': [49, 30], 'teleporter': [4, 6]}, {'name': 'Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin', 'id': 17, 'area': 10, 'coordinates': [47, 33], 'teleporter': [17, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin', 'id': 18, 'area': 10, 'coordinates': [47, 41], 'teleporter': [18, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor', 'id': 19, 'area': 10, 'coordinates': [38, 40], 'teleporter': [20, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor', 'id': 20, 'area': 10, 'coordinates': [56, 40], 'teleporter': [19, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - Pillar Script', 'id': 21, 'area': 10, 'coordinates': [48, 53], 'teleporter': [13, 8]}, {'name': 'Focus Tower 3F - Lower Floor - To Fireburg Entrance', 'id': 22, 'area': 11, 'coordinates': [11, 39], 'teleporter': [6, 6]}, {'name': 'Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar', 'id': 23, 'area': 11, 'coordinates': [6, 47], 'teleporter': [24, 0]}, {'name': 'Focus Tower 3F - Upper Floor - To Aquaria Entrance', 'id': 24, 'area': 11, 'coordinates': [21, 38], 'teleporter': [5, 6]}, {'name': 'Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room', 'id': 25, 'area': 11, 'coordinates': [24, 47], 'teleporter': [23, 0]}, {'name': 'Level Forest - Boulder Script', 'id': 26, 'area': 14, 'coordinates': [52, 15], 'teleporter': [0, 8]}, {'name': 'Level Forest - Rotten Tree Script', 'id': 27, 'area': 14, 'coordinates': [47, 6], 'teleporter': [2, 8]}, {'name': 'Level Forest - Exit Level Forest 1', 'id': 28, 'area': 14, 'coordinates': [46, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 2', 'id': 29, 'area': 14, 'coordinates': [46, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 3', 'id': 30, 'area': 14, 'coordinates': [47, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 4', 'id': 31, 'area': 14, 'coordinates': [47, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 5', 'id': 32, 'area': 14, 'coordinates': [60, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 6', 'id': 33, 'area': 14, 'coordinates': [61, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 7', 'id': 34, 'area': 14, 'coordinates': [46, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 8', 'id': 35, 'area': 14, 'coordinates': [46, 3], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 9', 'id': 36, 'area': 14, 'coordinates': [47, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest A', 'id': 37, 'area': 14, 'coordinates': [47, 3], 'teleporter': [25, 0]}, {'name': 'Foresta - Exit Foresta 1', 'id': 38, 'area': 15, 'coordinates': [10, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 2', 'id': 39, 'area': 15, 'coordinates': [10, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 3', 'id': 40, 'area': 15, 'coordinates': [11, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 4', 'id': 41, 'area': 15, 'coordinates': [11, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Old Man House - Front Door', 'id': 42, 'area': 15, 'coordinates': [25, 17], 'teleporter': [32, 4]}, {'name': 'Foresta - Old Man House - Back Door', 'id': 43, 'area': 15, 'coordinates': [25, 14], 'teleporter': [33, 0]}, {'name': "Foresta - Kaeli's House", 'id': 44, 'area': 15, 'coordinates': [7, 21], 'teleporter': [0, 5]}, {'name': 'Foresta - Rest House', 'id': 45, 'area': 15, 'coordinates': [23, 23], 'teleporter': [1, 5]}, {'name': "Kaeli's House - Kaeli's House Entrance", 'id': 46, 'area': 16, 'coordinates': [11, 20], 'teleporter': [86, 3]}, {'name': "Foresta Houses - Old Man's House - Old Man Front Exit", 'id': 47, 'area': 17, 'coordinates': [35, 44], 'teleporter': [34, 0]}, {'name': "Foresta Houses - Old Man's House - Old Man Back Exit", 'id': 48, 'area': 17, 'coordinates': [35, 27], 'teleporter': [35, 0]}, {'name': 'Foresta - Old Man House - Barrel Tile Script', 'id': 483, 'area': 17, 'coordinates': [35, 30], 'teleporter': [13, 8]}, {'name': 'Foresta Houses - Rest House - Bed Script', 'id': 49, 'area': 17, 'coordinates': [30, 6], 'teleporter': [1, 8]}, {'name': 'Foresta Houses - Rest House - Rest House Exit', 'id': 50, 'area': 17, 'coordinates': [35, 20], 'teleporter': [87, 3]}, {'name': 'Foresta Houses - Libra House - Libra House Script', 'id': 51, 'area': 17, 'coordinates': [8, 49], 'teleporter': [67, 8]}, {'name': 'Foresta Houses - Gemini House - Gemini House Script', 'id': 52, 'area': 17, 'coordinates': [26, 55], 'teleporter': [68, 8]}, {'name': 'Foresta Houses - Mobius House - Mobius House Script', 'id': 53, 'area': 17, 'coordinates': [14, 33], 'teleporter': [69, 8]}, {'name': 'Sand Temple - Sand Temple Entrance', 'id': 54, 'area': 18, 'coordinates': [56, 27], 'teleporter': [36, 0]}, {'name': 'Bone Dungeon 1F - Bone Dungeon Entrance', 'id': 55, 'area': 19, 'coordinates': [13, 60], 'teleporter': [37, 0]}, {'name': 'Bone Dungeon 1F - To Bone Dungeon B1', 'id': 56, 'area': 19, 'coordinates': [13, 39], 'teleporter': [2, 2]}, {'name': 'Bone Dungeon B1 - Waterway - Exit Waterway', 'id': 57, 'area': 20, 'coordinates': [27, 39], 'teleporter': [3, 2]}, {'name': "Bone Dungeon B1 - Waterway - Tristam's Script", 'id': 58, 'area': 20, 'coordinates': [27, 45], 'teleporter': [3, 8]}, {'name': 'Bone Dungeon B1 - Waterway - To Bone Dungeon 1F', 'id': 59, 'area': 20, 'coordinates': [54, 61], 'teleporter': [88, 3]}, {'name': 'Bone Dungeon B1 - Checker Room - Exit Checker Room', 'id': 60, 'area': 20, 'coordinates': [23, 40], 'teleporter': [4, 2]}, {'name': 'Bone Dungeon B1 - Checker Room - To Waterway', 'id': 61, 'area': 20, 'coordinates': [39, 49], 'teleporter': [89, 3]}, {'name': 'Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room', 'id': 62, 'area': 20, 'coordinates': [5, 33], 'teleporter': [91, 3]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage', 'id': 63, 'area': 21, 'coordinates': [19, 13], 'teleporter': [5, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room', 'id': 64, 'area': 21, 'coordinates': [29, 15], 'teleporter': [6, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Checker Room', 'id': 65, 'area': 21, 'coordinates': [8, 25], 'teleporter': [90, 3]}, {'name': 'Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room', 'id': 66, 'area': 21, 'coordinates': [59, 12], 'teleporter': [93, 3]}, {'name': 'Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room', 'id': 67, 'area': 21, 'coordinates': [59, 28], 'teleporter': [94, 3]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Box Room', 'id': 68, 'area': 21, 'coordinates': [53, 7], 'teleporter': [7, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Quake Room', 'id': 69, 'area': 21, 'coordinates': [41, 3], 'teleporter': [8, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Boss Room', 'id': 70, 'area': 21, 'coordinates': [47, 57], 'teleporter': [9, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room', 'id': 71, 'area': 21, 'coordinates': [54, 23], 'teleporter': [92, 3]}, {'name': 'Bone Dungeon B2 - Boss Room - Flamerus Rex Script', 'id': 72, 'area': 22, 'coordinates': [29, 19], 'teleporter': [4, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - Tristam Leave Script', 'id': 73, 'area': 22, 'coordinates': [29, 23], 'teleporter': [75, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room', 'id': 74, 'area': 22, 'coordinates': [30, 27], 'teleporter': [95, 3]}, {'name': 'Libra Temple - Entrance', 'id': 75, 'area': 23, 'coordinates': [10, 15], 'teleporter': [13, 6]}, {'name': 'Libra Temple - Libra Tile Script', 'id': 76, 'area': 23, 'coordinates': [9, 8], 'teleporter': [59, 8]}, {'name': 'Aquaria Winter - Winter Entrance 1', 'id': 77, 'area': 24, 'coordinates': [25, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 2', 'id': 78, 'area': 24, 'coordinates': [25, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 3', 'id': 79, 'area': 24, 'coordinates': [26, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 4', 'id': 80, 'area': 24, 'coordinates': [26, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Winter - Winter Phoebe's House Entrance Script", 'id': 81, 'area': 24, 'coordinates': [8, 19], 'teleporter': [10, 5]}, {'name': 'Aquaria Winter - Winter Vendor House Entrance', 'id': 82, 'area': 24, 'coordinates': [8, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Winter - Winter INN Entrance', 'id': 83, 'area': 24, 'coordinates': [26, 17], 'teleporter': [11, 5]}, {'name': 'Aquaria Summer - Summer Entrance 1', 'id': 84, 'area': 25, 'coordinates': [57, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 2', 'id': 85, 'area': 25, 'coordinates': [57, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 3', 'id': 86, 'area': 25, 'coordinates': [58, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 4', 'id': 87, 'area': 25, 'coordinates': [58, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Summer - Summer Phoebe's House Entrance", 'id': 88, 'area': 25, 'coordinates': [40, 19], 'teleporter': [10, 5]}, {'name': "Aquaria Summer - Spencer's Place Entrance Top", 'id': 89, 'area': 25, 'coordinates': [40, 16], 'teleporter': [42, 0]}, {'name': "Aquaria Summer - Spencer's Place Entrance Side", 'id': 90, 'area': 25, 'coordinates': [41, 18], 'teleporter': [43, 0]}, {'name': 'Aquaria Summer - Summer Vendor House Entrance', 'id': 91, 'area': 25, 'coordinates': [40, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Summer - Summer INN Entrance', 'id': 92, 'area': 25, 'coordinates': [58, 17], 'teleporter': [11, 5]}, {'name': "Phoebe's House - Entrance", 'id': 93, 'area': 26, 'coordinates': [29, 14], 'teleporter': [5, 8]}, {'name': "Aquaria Vendor House - Vendor House Entrance's Script", 'id': 94, 'area': 27, 'coordinates': [7, 10], 'teleporter': [40, 8]}, {'name': 'Aquaria Vendor House - Vendor House Stairs', 'id': 95, 'area': 27, 'coordinates': [1, 4], 'teleporter': [47, 0]}, {'name': 'Aquaria Gemini Room - Gemini Script', 'id': 96, 'area': 27, 'coordinates': [2, 40], 'teleporter': [72, 8]}, {'name': 'Aquaria Gemini Room - Gemini Room Stairs', 'id': 97, 'area': 27, 'coordinates': [4, 39], 'teleporter': [48, 0]}, {'name': 'Aquaria INN - Aquaria INN entrance', 'id': 98, 'area': 27, 'coordinates': [51, 46], 'teleporter': [75, 8]}, {'name': 'Wintry Cave 1F - Main Entrance', 'id': 99, 'area': 28, 'coordinates': [50, 58], 'teleporter': [49, 0]}, {'name': 'Wintry Cave 1F - To 3F Top', 'id': 100, 'area': 28, 'coordinates': [40, 25], 'teleporter': [14, 2]}, {'name': 'Wintry Cave 1F - To 2F', 'id': 101, 'area': 28, 'coordinates': [10, 43], 'teleporter': [15, 2]}, {'name': "Wintry Cave 1F - Phoebe's Script", 'id': 102, 'area': 28, 'coordinates': [44, 37], 'teleporter': [6, 8]}, {'name': 'Wintry Cave 2F - To 3F Bottom', 'id': 103, 'area': 29, 'coordinates': [58, 5], 'teleporter': [50, 0]}, {'name': 'Wintry Cave 2F - To 1F', 'id': 104, 'area': 29, 'coordinates': [38, 18], 'teleporter': [97, 3]}, {'name': 'Wintry Cave 3F Top - Exit from 3F Top', 'id': 105, 'area': 30, 'coordinates': [24, 6], 'teleporter': [96, 3]}, {'name': 'Wintry Cave 3F Bottom - Exit to 2F', 'id': 106, 'area': 31, 'coordinates': [4, 29], 'teleporter': [51, 0]}, {'name': 'Life Temple - Entrance', 'id': 107, 'area': 32, 'coordinates': [9, 60], 'teleporter': [14, 6]}, {'name': 'Life Temple - Libra Tile Script', 'id': 108, 'area': 32, 'coordinates': [3, 55], 'teleporter': [60, 8]}, {'name': 'Life Temple - Mysterious Man Script', 'id': 109, 'area': 32, 'coordinates': [9, 44], 'teleporter': [78, 8]}, {'name': 'Fall Basin - Back Exit Script', 'id': 110, 'area': 33, 'coordinates': [17, 5], 'teleporter': [9, 0]}, {'name': 'Fall Basin - Main Exit', 'id': 111, 'area': 33, 'coordinates': [15, 26], 'teleporter': [53, 0]}, {'name': "Fall Basin - Phoebe's Script", 'id': 112, 'area': 33, 'coordinates': [17, 6], 'teleporter': [9, 8]}, {'name': 'Ice Pyramid B1 Taunt Room - To Climbing Wall Room', 'id': 113, 'area': 34, 'coordinates': [43, 6], 'teleporter': [55, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 1', 'id': 114, 'area': 35, 'coordinates': [18, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 2', 'id': 115, 'area': 35, 'coordinates': [19, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room', 'id': 116, 'area': 35, 'coordinates': [3, 27], 'teleporter': [57, 0]}, {'name': 'Ice Pyramid 1F Maze - West Center Stairs to 2F West Room', 'id': 117, 'area': 35, 'coordinates': [11, 15], 'teleporter': [58, 0]}, {'name': 'Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room', 'id': 118, 'area': 35, 'coordinates': [25, 16], 'teleporter': [59, 0]}, {'name': 'Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room', 'id': 119, 'area': 35, 'coordinates': [31, 1], 'teleporter': [60, 0]}, {'name': 'Ice Pyramid 1F Maze - East Stairs to 2F North Corridor', 'id': 120, 'area': 35, 'coordinates': [34, 9], 'teleporter': [61, 0]}, {'name': "Ice Pyramid 1F Maze - Statue's Script", 'id': 121, 'area': 35, 'coordinates': [21, 32], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 1F', 'id': 122, 'area': 36, 'coordinates': [4, 26], 'teleporter': [62, 0]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room', 'id': 123, 'area': 36, 'coordinates': [22, 17], 'teleporter': [67, 0]}, {'name': 'Ice Pyramid 2F West Room - To 1F', 'id': 124, 'area': 36, 'coordinates': [9, 10], 'teleporter': [63, 0]}, {'name': 'Ice Pyramid 2F Center Room - To 1F', 'id': 125, 'area': 36, 'coordinates': [22, 14], 'teleporter': [64, 0]}, {'name': 'Ice Pyramid 2F Small North Room - To 1F', 'id': 126, 'area': 36, 'coordinates': [26, 4], 'teleporter': [65, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 1F', 'id': 127, 'area': 36, 'coordinates': [32, 8], 'teleporter': [66, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 3F Main Loop', 'id': 128, 'area': 36, 'coordinates': [12, 7], 'teleporter': [68, 0]}, {'name': 'Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room', 'id': 129, 'area': 37, 'coordinates': [24, 54], 'teleporter': [69, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 2F Corridor', 'id': 130, 'area': 37, 'coordinates': [16, 45], 'teleporter': [70, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 4F', 'id': 131, 'area': 37, 'coordinates': [19, 43], 'teleporter': [71, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 3F Main Loop', 'id': 132, 'area': 38, 'coordinates': [52, 5], 'teleporter': [72, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room', 'id': 133, 'area': 38, 'coordinates': [62, 19], 'teleporter': [73, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room', 'id': 134, 'area': 39, 'coordinates': [54, 63], 'teleporter': [74, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate', 'id': 135, 'area': 39, 'coordinates': [47, 54], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room', 'id': 136, 'area': 39, 'coordinates': [39, 43], 'teleporter': [75, 0]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room', 'id': 137, 'area': 39, 'coordinates': [39, 60], 'teleporter': [76, 0]}, {'name': 'Ice Pyramid - Duplicate Ice Golem Room', 'id': 138, 'area': 40, 'coordinates': [44, 43], 'teleporter': [77, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To Taunt Room', 'id': 139, 'area': 41, 'coordinates': [4, 59], 'teleporter': [78, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To 5F Stairs', 'id': 140, 'area': 41, 'coordinates': [4, 45], 'teleporter': [79, 0]}, {'name': 'Ice Pyramid Ice Golem Room - To 5F Stairs', 'id': 141, 'area': 42, 'coordinates': [44, 43], 'teleporter': [80, 0]}, {'name': 'Ice Pyramid Ice Golem Room - Ice Golem Script', 'id': 142, 'area': 42, 'coordinates': [53, 32], 'teleporter': [10, 8]}, {'name': 'Spencer Waterfall - To Spencer Cave', 'id': 143, 'area': 43, 'coordinates': [48, 57], 'teleporter': [81, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 1', 'id': 144, 'area': 43, 'coordinates': [40, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 2', 'id': 145, 'area': 43, 'coordinates': [40, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 3', 'id': 146, 'area': 43, 'coordinates': [41, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 4', 'id': 147, 'area': 43, 'coordinates': [41, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 1', 'id': 148, 'area': 43, 'coordinates': [46, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 2', 'id': 149, 'area': 43, 'coordinates': [47, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Cave Normal Main - To Waterfall', 'id': 150, 'area': 44, 'coordinates': [14, 39], 'teleporter': [85, 0]}, {'name': 'Spencer Cave Normal From Overworld - Exit to Overworld', 'id': 151, 'area': 44, 'coordinates': [15, 57], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Exit to Overworld', 'id': 152, 'area': 45, 'coordinates': [40, 29], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Libra Teleporter Start Script', 'id': 153, 'area': 45, 'coordinates': [28, 21], 'teleporter': [33, 8]}, {'name': 'Spencer Cave Unplug - Libra Teleporter End Script', 'id': 154, 'area': 45, 'coordinates': [46, 4], 'teleporter': [34, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Chest Script', 'id': 155, 'area': 45, 'coordinates': [21, 9], 'teleporter': [35, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Start Script', 'id': 156, 'area': 45, 'coordinates': [29, 28], 'teleporter': [36, 8]}, {'name': 'Wintry Temple Outer Room - Main Entrance', 'id': 157, 'area': 46, 'coordinates': [8, 31], 'teleporter': [15, 6]}, {'name': 'Wintry Temple Inner Room - Gemini Tile to Sealed temple', 'id': 158, 'area': 46, 'coordinates': [9, 24], 'teleporter': [62, 8]}, {'name': 'Fireburg - To Overworld', 'id': 159, 'area': 47, 'coordinates': [4, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 160, 'area': 47, 'coordinates': [5, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 161, 'area': 47, 'coordinates': [28, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 162, 'area': 47, 'coordinates': [27, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - Vendor House', 'id': 163, 'area': 47, 'coordinates': [10, 24], 'teleporter': [91, 0]}, {'name': 'Fireburg - Reuben House', 'id': 164, 'area': 47, 'coordinates': [14, 6], 'teleporter': [98, 8]}, {'name': 'Fireburg - Hotel', 'id': 165, 'area': 47, 'coordinates': [20, 8], 'teleporter': [96, 8]}, {'name': 'Fireburg - GrenadeMan House Script', 'id': 166, 'area': 47, 'coordinates': [12, 18], 'teleporter': [11, 8]}, {'name': 'Reuben House - Main Entrance', 'id': 167, 'area': 48, 'coordinates': [33, 46], 'teleporter': [98, 3]}, {'name': 'GrenadeMan House - Entrance Script', 'id': 168, 'area': 49, 'coordinates': [55, 60], 'teleporter': [9, 8]}, {'name': 'GrenadeMan House - To Mobius Crest Room', 'id': 169, 'area': 49, 'coordinates': [57, 52], 'teleporter': [93, 0]}, {'name': 'GrenadeMan Mobius Room - Stairs to House', 'id': 170, 'area': 49, 'coordinates': [39, 26], 'teleporter': [94, 0]}, {'name': 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'id': 171, 'area': 49, 'coordinates': [39, 23], 'teleporter': [54, 8]}, {'name': 'Fireburg Vendor House - Entrance Script', 'id': 172, 'area': 49, 'coordinates': [7, 10], 'teleporter': [95, 0]}, {'name': 'Fireburg Vendor House - Stairs to Gemini Room', 'id': 173, 'area': 49, 'coordinates': [1, 4], 'teleporter': [96, 0]}, {'name': 'Fireburg Gemini Room - Stairs to Vendor House', 'id': 174, 'area': 49, 'coordinates': [4, 39], 'teleporter': [97, 0]}, {'name': 'Fireburg Gemini Room - Gemini Teleporter Script', 'id': 175, 'area': 49, 'coordinates': [2, 40], 'teleporter': [45, 8]}, {'name': 'Fireburg Hotel Lobby - Stairs to beds', 'id': 176, 'area': 49, 'coordinates': [4, 50], 'teleporter': [213, 0]}, {'name': 'Fireburg Hotel Lobby - Entrance', 'id': 177, 'area': 49, 'coordinates': [17, 56], 'teleporter': [99, 3]}, {'name': 'Fireburg Hotel Beds - Stairs to Hotel Lobby', 'id': 178, 'area': 49, 'coordinates': [45, 59], 'teleporter': [214, 0]}, {'name': 'Mine Exterior - Main Entrance', 'id': 179, 'area': 50, 'coordinates': [5, 28], 'teleporter': [98, 0]}, {'name': 'Mine Exterior - To Cliff', 'id': 180, 'area': 50, 'coordinates': [58, 29], 'teleporter': [99, 0]}, {'name': 'Mine Exterior - To Parallel Room', 'id': 181, 'area': 50, 'coordinates': [8, 7], 'teleporter': [20, 2]}, {'name': 'Mine Exterior - To Crescent Room', 'id': 182, 'area': 50, 'coordinates': [26, 15], 'teleporter': [21, 2]}, {'name': 'Mine Exterior - To Climbing Room', 'id': 183, 'area': 50, 'coordinates': [21, 35], 'teleporter': [22, 2]}, {'name': 'Mine Exterior - Jinn Fight Script', 'id': 184, 'area': 50, 'coordinates': [58, 31], 'teleporter': [74, 8]}, {'name': 'Mine Parallel Room - To Mine Exterior', 'id': 185, 'area': 51, 'coordinates': [7, 60], 'teleporter': [100, 3]}, {'name': 'Mine Crescent Room - To Mine Exterior', 'id': 186, 'area': 51, 'coordinates': [22, 61], 'teleporter': [101, 3]}, {'name': 'Mine Climbing Room - To Mine Exterior', 'id': 187, 'area': 51, 'coordinates': [56, 21], 'teleporter': [102, 3]}, {'name': 'Mine Cliff - Entrance', 'id': 188, 'area': 52, 'coordinates': [9, 5], 'teleporter': [100, 0]}, {'name': 'Mine Cliff - Reuben Grenade Script', 'id': 189, 'area': 52, 'coordinates': [15, 7], 'teleporter': [12, 8]}, {'name': 'Sealed Temple - To Overworld', 'id': 190, 'area': 53, 'coordinates': [58, 43], 'teleporter': [16, 6]}, {'name': 'Sealed Temple - Gemini Tile Script', 'id': 191, 'area': 53, 'coordinates': [56, 38], 'teleporter': [63, 8]}, {'name': 'Volcano Base - Main Entrance 1', 'id': 192, 'area': 54, 'coordinates': [23, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 2', 'id': 193, 'area': 54, 'coordinates': [23, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 3', 'id': 194, 'area': 54, 'coordinates': [24, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 4', 'id': 195, 'area': 54, 'coordinates': [24, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Left Stairs Script', 'id': 196, 'area': 54, 'coordinates': [20, 5], 'teleporter': [31, 8]}, {'name': 'Volcano Base - Right Stairs Script', 'id': 197, 'area': 54, 'coordinates': [32, 5], 'teleporter': [30, 8]}, {'name': 'Volcano Top Right - Top Exit', 'id': 198, 'area': 55, 'coordinates': [44, 8], 'teleporter': [9, 0]}, {'name': 'Volcano Top Left - To Right-Left Path Script', 'id': 199, 'area': 55, 'coordinates': [40, 24], 'teleporter': [26, 8]}, {'name': 'Volcano Top Right - To Left-Right Path Script', 'id': 200, 'area': 55, 'coordinates': [52, 24], 'teleporter': [79, 8]}, {'name': 'Volcano Right Path - To Volcano Base Script', 'id': 201, 'area': 56, 'coordinates': [48, 42], 'teleporter': [15, 8]}, {'name': 'Volcano Left Path - To Volcano Cross Left-Right', 'id': 202, 'area': 56, 'coordinates': [40, 31], 'teleporter': [25, 2]}, {'name': 'Volcano Left Path - To Volcano Cross Right-Left', 'id': 203, 'area': 56, 'coordinates': [52, 29], 'teleporter': [26, 2]}, {'name': 'Volcano Left Path - To Volcano Base Script', 'id': 204, 'area': 56, 'coordinates': [36, 42], 'teleporter': [27, 8]}, {'name': 'Volcano Cross Left-Right - To Volcano Left Path', 'id': 205, 'area': 56, 'coordinates': [10, 42], 'teleporter': [103, 3]}, {'name': 'Volcano Cross Left-Right - To Volcano Top Right Script', 'id': 206, 'area': 56, 'coordinates': [16, 24], 'teleporter': [29, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Top Left Script', 'id': 207, 'area': 56, 'coordinates': [8, 22], 'teleporter': [28, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Left Path', 'id': 208, 'area': 56, 'coordinates': [16, 42], 'teleporter': [104, 3]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 1', 'id': 209, 'area': 57, 'coordinates': [32, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 2', 'id': 210, 'area': 57, 'coordinates': [33, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Three Steps Room', 'id': 211, 'area': 57, 'coordinates': [14, 5], 'teleporter': [105, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Life Chest Room Lower', 'id': 212, 'area': 57, 'coordinates': [40, 17], 'teleporter': [106, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Big Jump Room Left', 'id': 213, 'area': 57, 'coordinates': [8, 11], 'teleporter': [108, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Split Corridor Room', 'id': 214, 'area': 57, 'coordinates': [11, 19], 'teleporter': [111, 0]}, {'name': 'Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher', 'id': 215, 'area': 57, 'coordinates': [32, 11], 'teleporter': [107, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - To Plate Corridor', 'id': 216, 'area': 57, 'coordinates': [12, 23], 'teleporter': [109, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - Plate Script', 'id': 217, 'area': 57, 'coordinates': [5, 23], 'teleporter': [47, 8]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Pointless Room', 'id': 218, 'area': 57, 'coordinates': [0, 9], 'teleporter': [110, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room', 'id': 219, 'area': 57, 'coordinates': [0, 15], 'teleporter': [112, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor', 'id': 220, 'area': 57, 'coordinates': [54, 5], 'teleporter': [113, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II', 'id': 221, 'area': 57, 'coordinates': [54, 21], 'teleporter': [114, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1', 'id': 222, 'area': 57, 'coordinates': [62, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2', 'id': 223, 'area': 57, 'coordinates': [63, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3', 'id': 224, 'area': 57, 'coordinates': [62, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4', 'id': 225, 'area': 57, 'coordinates': [63, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor', 'id': 226, 'area': 57, 'coordinates': [50, 25], 'teleporter': [115, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Right Entrance', 'id': 227, 'area': 58, 'coordinates': [55, 28], 'teleporter': [116, 0]}, {'name': 'Lava Dome Jump Maze II - Upper Entrance', 'id': 228, 'area': 58, 'coordinates': [35, 3], 'teleporter': [119, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Left Entrance', 'id': 229, 'area': 58, 'coordinates': [34, 27], 'teleporter': [120, 0]}, {'name': 'Lava Dome Up-Down Corridor - Upper Entrance', 'id': 230, 'area': 58, 'coordinates': [29, 8], 'teleporter': [117, 0]}, {'name': 'Lava Dome Up-Down Corridor - Lower Entrance', 'id': 231, 'area': 58, 'coordinates': [28, 25], 'teleporter': [118, 0]}, {'name': 'Lava Dome Jump Maze I - South Entrance', 'id': 232, 'area': 59, 'coordinates': [20, 27], 'teleporter': [121, 0]}, {'name': 'Lava Dome Jump Maze I - North Entrance', 'id': 233, 'area': 59, 'coordinates': [7, 3], 'teleporter': [122, 0]}, {'name': 'Lava Dome Pointless Room - Entrance', 'id': 234, 'area': 60, 'coordinates': [2, 7], 'teleporter': [123, 0]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 1', 'id': 490, 'area': 60, 'coordinates': [4, 4], 'teleporter': [99, 8]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 2', 'id': 491, 'area': 60, 'coordinates': [4, 5], 'teleporter': [99, 8]}, {'name': 'Lava Dome Lower Moon Helm Room - Left Entrance', 'id': 235, 'area': 60, 'coordinates': [2, 19], 'teleporter': [124, 0]}, {'name': 'Lava Dome Lower Moon Helm Room - Right Entrance', 'id': 236, 'area': 60, 'coordinates': [11, 21], 'teleporter': [125, 0]}, {'name': 'Lava Dome Moon Helm Room - Entrance', 'id': 237, 'area': 60, 'coordinates': [15, 23], 'teleporter': [126, 0]}, {'name': 'Lava Dome Three Jumps Room - To Main Loop', 'id': 238, 'area': 61, 'coordinates': [58, 15], 'teleporter': [127, 0]}, {'name': 'Lava Dome Life Chest Room - Lower South Entrance', 'id': 239, 'area': 61, 'coordinates': [38, 27], 'teleporter': [128, 0]}, {'name': 'Lava Dome Life Chest Room - Upper South Entrance', 'id': 240, 'area': 61, 'coordinates': [28, 23], 'teleporter': [129, 0]}, {'name': 'Lava Dome Big Jump Room - Left Entrance', 'id': 241, 'area': 62, 'coordinates': [42, 51], 'teleporter': [133, 0]}, {'name': 'Lava Dome Big Jump Room - North Entrance', 'id': 242, 'area': 62, 'coordinates': [30, 29], 'teleporter': [131, 0]}, {'name': 'Lava Dome Big Jump Room - Lower Right Stairs', 'id': 243, 'area': 62, 'coordinates': [61, 59], 'teleporter': [132, 0]}, {'name': 'Lava Dome Split Corridor - Upper Stairs', 'id': 244, 'area': 62, 'coordinates': [30, 43], 'teleporter': [130, 0]}, {'name': 'Lava Dome Split Corridor - Lower Stairs', 'id': 245, 'area': 62, 'coordinates': [36, 61], 'teleporter': [134, 0]}, {'name': 'Lava Dome Plate Corridor - Right Entrance', 'id': 246, 'area': 63, 'coordinates': [19, 29], 'teleporter': [135, 0]}, {'name': 'Lava Dome Plate Corridor - Left Entrance', 'id': 247, 'area': 63, 'coordinates': [60, 21], 'teleporter': [137, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Upper Entrance', 'id': 248, 'area': 63, 'coordinates': [22, 3], 'teleporter': [136, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Lower Entrance', 'id': 249, 'area': 63, 'coordinates': [22, 17], 'teleporter': [16, 0]}, {'name': 'Lava Dome Hydra Room - South Entrance', 'id': 250, 'area': 64, 'coordinates': [14, 59], 'teleporter': [105, 3]}, {'name': 'Lava Dome Hydra Room - North Exit', 'id': 251, 'area': 64, 'coordinates': [25, 31], 'teleporter': [138, 0]}, {'name': 'Lava Dome Hydra Room - Hydra Script', 'id': 252, 'area': 64, 'coordinates': [14, 36], 'teleporter': [14, 8]}, {'name': 'Lava Dome Escape Corridor - South Entrance', 'id': 253, 'area': 65, 'coordinates': [22, 17], 'teleporter': [139, 0]}, {'name': 'Lava Dome Escape Corridor - North Entrance', 'id': 254, 'area': 65, 'coordinates': [22, 3], 'teleporter': [9, 0]}, {'name': 'Rope Bridge - West Entrance 1', 'id': 255, 'area': 66, 'coordinates': [3, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 2', 'id': 256, 'area': 66, 'coordinates': [3, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 3', 'id': 257, 'area': 66, 'coordinates': [3, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 4', 'id': 258, 'area': 66, 'coordinates': [3, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 5', 'id': 259, 'area': 66, 'coordinates': [4, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 6', 'id': 260, 'area': 66, 'coordinates': [4, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 7', 'id': 261, 'area': 66, 'coordinates': [4, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 8', 'id': 262, 'area': 66, 'coordinates': [4, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 1', 'id': 263, 'area': 66, 'coordinates': [59, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 2', 'id': 264, 'area': 66, 'coordinates': [59, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 3', 'id': 265, 'area': 66, 'coordinates': [59, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 4', 'id': 266, 'area': 66, 'coordinates': [59, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 5', 'id': 267, 'area': 66, 'coordinates': [60, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 6', 'id': 268, 'area': 66, 'coordinates': [60, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 7', 'id': 269, 'area': 66, 'coordinates': [60, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 8', 'id': 270, 'area': 66, 'coordinates': [60, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - Reuben Fall Script', 'id': 271, 'area': 66, 'coordinates': [13, 12], 'teleporter': [15, 8]}, {'name': 'Alive Forest - West Entrance 1', 'id': 272, 'area': 67, 'coordinates': [8, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - West Entrance 2', 'id': 273, 'area': 67, 'coordinates': [9, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - Giant Tree Entrance', 'id': 274, 'area': 67, 'coordinates': [42, 42], 'teleporter': [143, 0]}, {'name': 'Alive Forest - Libra Teleporter Script', 'id': 275, 'area': 67, 'coordinates': [8, 52], 'teleporter': [64, 8]}, {'name': 'Alive Forest - Gemini Teleporter Script', 'id': 276, 'area': 67, 'coordinates': [57, 49], 'teleporter': [65, 8]}, {'name': 'Alive Forest - Mobius Teleporter Script', 'id': 277, 'area': 67, 'coordinates': [24, 10], 'teleporter': [66, 8]}, {'name': 'Giant Tree 1F - Entrance Script 1', 'id': 278, 'area': 68, 'coordinates': [18, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - Entrance Script 2', 'id': 279, 'area': 68, 'coordinates': [19, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - North Entrance To 2F', 'id': 280, 'area': 68, 'coordinates': [16, 1], 'teleporter': [144, 0]}, {'name': 'Giant Tree 2F Main Lobby - North Entrance to 1F', 'id': 281, 'area': 69, 'coordinates': [44, 33], 'teleporter': [145, 0]}, {'name': 'Giant Tree 2F Main Lobby - Central Entrance to 3F', 'id': 282, 'area': 69, 'coordinates': [42, 47], 'teleporter': [146, 0]}, {'name': 'Giant Tree 2F Main Lobby - West Entrance to Mushroom Room', 'id': 283, 'area': 69, 'coordinates': [58, 49], 'teleporter': [149, 0]}, {'name': 'Giant Tree 2F West Ledge - To 3F Northwest Ledge', 'id': 284, 'area': 69, 'coordinates': [34, 37], 'teleporter': [147, 0]}, {'name': 'Giant Tree 2F Fall From Vine Script', 'id': 482, 'area': 69, 'coordinates': [46, 51], 'teleporter': [76, 8]}, {'name': 'Giant Tree Meteor Chest Room - To 2F Mushroom Room', 'id': 285, 'area': 69, 'coordinates': [58, 44], 'teleporter': [148, 0]}, {'name': 'Giant Tree 2F Mushroom Room - Entrance', 'id': 286, 'area': 70, 'coordinates': [55, 18], 'teleporter': [150, 0]}, {'name': 'Giant Tree 2F Mushroom Room - North Face to Meteor', 'id': 287, 'area': 70, 'coordinates': [56, 7], 'teleporter': [151, 0]}, {'name': 'Giant Tree 3F Central Room - Central Entrance to 2F', 'id': 288, 'area': 71, 'coordinates': [46, 53], 'teleporter': [152, 0]}, {'name': 'Giant Tree 3F Central Room - East Entrance to Worm Room', 'id': 289, 'area': 71, 'coordinates': [58, 39], 'teleporter': [153, 0]}, {'name': 'Giant Tree 3F Lower Corridor - Entrance from Worm Room', 'id': 290, 'area': 71, 'coordinates': [45, 39], 'teleporter': [154, 0]}, {'name': 'Giant Tree 3F West Platform - Lower Entrance', 'id': 291, 'area': 71, 'coordinates': [33, 43], 'teleporter': [155, 0]}, {'name': 'Giant Tree 3F West Platform - Top Entrance', 'id': 292, 'area': 71, 'coordinates': [52, 25], 'teleporter': [156, 0]}, {'name': 'Giant Tree Worm Room - East Entrance', 'id': 293, 'area': 72, 'coordinates': [20, 58], 'teleporter': [157, 0]}, {'name': 'Giant Tree Worm Room - West Entrance', 'id': 294, 'area': 72, 'coordinates': [6, 56], 'teleporter': [158, 0]}, {'name': 'Giant Tree 4F Lower Floor - Entrance', 'id': 295, 'area': 73, 'coordinates': [20, 7], 'teleporter': [159, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower West Mouth', 'id': 296, 'area': 73, 'coordinates': [8, 23], 'teleporter': [160, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower Central Mouth', 'id': 297, 'area': 73, 'coordinates': [14, 25], 'teleporter': [161, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower East Mouth', 'id': 298, 'area': 73, 'coordinates': [20, 25], 'teleporter': [162, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper West Mouth', 'id': 299, 'area': 73, 'coordinates': [8, 19], 'teleporter': [163, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper Central Mouth', 'id': 300, 'area': 73, 'coordinates': [12, 17], 'teleporter': [164, 0]}, {'name': 'Giant Tree 4F Slime Room - Exit', 'id': 301, 'area': 74, 'coordinates': [47, 10], 'teleporter': [165, 0]}, {'name': 'Giant Tree 4F Slime Room - West Entrance', 'id': 302, 'area': 74, 'coordinates': [45, 24], 'teleporter': [166, 0]}, {'name': 'Giant Tree 4F Slime Room - Central Entrance', 'id': 303, 'area': 74, 'coordinates': [50, 24], 'teleporter': [167, 0]}, {'name': 'Giant Tree 4F Slime Room - East Entrance', 'id': 304, 'area': 74, 'coordinates': [57, 28], 'teleporter': [168, 0]}, {'name': 'Giant Tree 5F - Entrance', 'id': 305, 'area': 75, 'coordinates': [14, 51], 'teleporter': [169, 0]}, {'name': 'Giant Tree 5F - Giant Tree Face', 'id': 306, 'area': 75, 'coordinates': [14, 37], 'teleporter': [170, 0]}, {'name': 'Kaidge Temple - Entrance', 'id': 307, 'area': 77, 'coordinates': [44, 63], 'teleporter': [18, 6]}, {'name': 'Kaidge Temple - Mobius Teleporter Script', 'id': 308, 'area': 77, 'coordinates': [35, 57], 'teleporter': [71, 8]}, {'name': 'Windhole Temple - Entrance', 'id': 309, 'area': 78, 'coordinates': [10, 29], 'teleporter': [173, 0]}, {'name': 'Mount Gale - Entrance 1', 'id': 310, 'area': 79, 'coordinates': [1, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Entrance 2', 'id': 311, 'area': 79, 'coordinates': [2, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Visit Quest', 'id': 494, 'area': 79, 'coordinates': [44, 7], 'teleporter': [101, 8]}, {'name': 'Windia - Main Entrance 1', 'id': 312, 'area': 80, 'coordinates': [12, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 2', 'id': 313, 'area': 80, 'coordinates': [13, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 3', 'id': 314, 'area': 80, 'coordinates': [14, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 4', 'id': 315, 'area': 80, 'coordinates': [15, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 5', 'id': 316, 'area': 80, 'coordinates': [12, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 6', 'id': 317, 'area': 80, 'coordinates': [13, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 7', 'id': 318, 'area': 80, 'coordinates': [14, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 8', 'id': 319, 'area': 80, 'coordinates': [15, 41], 'teleporter': [10, 6]}, {'name': "Windia - Otto's House", 'id': 320, 'area': 80, 'coordinates': [21, 39], 'teleporter': [30, 5]}, {'name': "Windia - INN's Script", 'id': 321, 'area': 80, 'coordinates': [18, 34], 'teleporter': [97, 8]}, {'name': 'Windia - Vendor House', 'id': 322, 'area': 80, 'coordinates': [8, 36], 'teleporter': [32, 5]}, {'name': 'Windia - Kid House', 'id': 323, 'area': 80, 'coordinates': [7, 23], 'teleporter': [176, 4]}, {'name': 'Windia - Old People House', 'id': 324, 'area': 80, 'coordinates': [19, 21], 'teleporter': [177, 4]}, {'name': 'Windia - Rainbow Bridge Script', 'id': 325, 'area': 80, 'coordinates': [21, 9], 'teleporter': [10, 6]}, {'name': "Otto's House - Attic Stairs", 'id': 326, 'area': 81, 'coordinates': [2, 19], 'teleporter': [33, 2]}, {'name': "Otto's House - Entrance", 'id': 327, 'area': 81, 'coordinates': [9, 30], 'teleporter': [106, 3]}, {'name': "Otto's Attic - Stairs", 'id': 328, 'area': 81, 'coordinates': [26, 23], 'teleporter': [107, 3]}, {'name': 'Windia Kid House - Entrance Script', 'id': 329, 'area': 82, 'coordinates': [7, 10], 'teleporter': [178, 0]}, {'name': 'Windia Kid House - Basement Stairs', 'id': 330, 'area': 82, 'coordinates': [1, 4], 'teleporter': [180, 0]}, {'name': 'Windia Old People House - Entrance', 'id': 331, 'area': 82, 'coordinates': [55, 12], 'teleporter': [179, 0]}, {'name': 'Windia Old People House - Basement Stairs', 'id': 332, 'area': 82, 'coordinates': [60, 5], 'teleporter': [181, 0]}, {'name': 'Windia Kid House Basement - Stairs', 'id': 333, 'area': 82, 'coordinates': [43, 8], 'teleporter': [182, 0]}, {'name': 'Windia Kid House Basement - Mobius Teleporter', 'id': 334, 'area': 82, 'coordinates': [41, 9], 'teleporter': [44, 8]}, {'name': 'Windia Old People House Basement - Stairs', 'id': 335, 'area': 82, 'coordinates': [39, 26], 'teleporter': [183, 0]}, {'name': 'Windia Old People House Basement - Mobius Teleporter Script', 'id': 336, 'area': 82, 'coordinates': [39, 23], 'teleporter': [43, 8]}, {'name': 'Windia Inn Lobby - Stairs to Beds', 'id': 337, 'area': 82, 'coordinates': [45, 24], 'teleporter': [102, 8]}, {'name': 'Windia Inn Lobby - Exit', 'id': 338, 'area': 82, 'coordinates': [53, 30], 'teleporter': [135, 3]}, {'name': 'Windia Inn Beds - Stairs to Lobby', 'id': 339, 'area': 82, 'coordinates': [33, 59], 'teleporter': [216, 0]}, {'name': 'Windia Vendor House - Entrance', 'id': 340, 'area': 82, 'coordinates': [29, 14], 'teleporter': [108, 3]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 1', 'id': 341, 'area': 83, 'coordinates': [47, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 2', 'id': 342, 'area': 83, 'coordinates': [47, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 3', 'id': 343, 'area': 83, 'coordinates': [48, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 4', 'id': 344, 'area': 83, 'coordinates': [48, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - East Entrance', 'id': 345, 'area': 83, 'coordinates': [55, 12], 'teleporter': [185, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - South Stairs', 'id': 346, 'area': 83, 'coordinates': [51, 25], 'teleporter': [186, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 1', 'id': 347, 'area': 83, 'coordinates': [47, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 2', 'id': 348, 'area': 83, 'coordinates': [48, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Boxes Room - West Stairs', 'id': 349, 'area': 83, 'coordinates': [38, 17], 'teleporter': [187, 0]}, {'name': 'Pazuzu 2F - West Upper Stairs', 'id': 350, 'area': 84, 'coordinates': [7, 11], 'teleporter': [188, 0]}, {'name': 'Pazuzu 2F - South Stairs', 'id': 351, 'area': 84, 'coordinates': [20, 24], 'teleporter': [189, 0]}, {'name': 'Pazuzu 2F - West Lower Stairs', 'id': 352, 'area': 84, 'coordinates': [6, 17], 'teleporter': [190, 0]}, {'name': 'Pazuzu 2F - Central Stairs', 'id': 353, 'area': 84, 'coordinates': [15, 15], 'teleporter': [191, 0]}, {'name': 'Pazuzu 2F - Pazuzu 1', 'id': 354, 'area': 84, 'coordinates': [15, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 2F - Pazuzu 2', 'id': 355, 'area': 84, 'coordinates': [16, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 3F Main Room - North Stairs', 'id': 356, 'area': 85, 'coordinates': [23, 11], 'teleporter': [192, 0]}, {'name': 'Pazuzu 3F Main Room - West Stairs', 'id': 357, 'area': 85, 'coordinates': [7, 15], 'teleporter': [193, 0]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 1', 'id': 358, 'area': 85, 'coordinates': [15, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 2', 'id': 359, 'area': 85, 'coordinates': [16, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Central Island - Central Stairs', 'id': 360, 'area': 85, 'coordinates': [15, 14], 'teleporter': [194, 0]}, {'name': 'Pazuzu 3F Central Island - South Stairs', 'id': 361, 'area': 85, 'coordinates': [17, 25], 'teleporter': [195, 0]}, {'name': 'Pazuzu 4F - Northwest Stairs', 'id': 362, 'area': 86, 'coordinates': [39, 12], 'teleporter': [196, 0]}, {'name': 'Pazuzu 4F - Southwest Stairs', 'id': 363, 'area': 86, 'coordinates': [39, 19], 'teleporter': [197, 0]}, {'name': 'Pazuzu 4F - South Stairs', 'id': 364, 'area': 86, 'coordinates': [47, 24], 'teleporter': [198, 0]}, {'name': 'Pazuzu 4F - Northeast Stairs', 'id': 365, 'area': 86, 'coordinates': [54, 9], 'teleporter': [199, 0]}, {'name': 'Pazuzu 4F - Pazuzu Script 1', 'id': 366, 'area': 86, 'coordinates': [47, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 4F - Pazuzu Script 2', 'id': 367, 'area': 86, 'coordinates': [48, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 5F Pazuzu Loop - West Stairs', 'id': 368, 'area': 87, 'coordinates': [9, 49], 'teleporter': [200, 0]}, {'name': 'Pazuzu 5F Pazuzu Loop - South Stairs', 'id': 369, 'area': 87, 'coordinates': [16, 55], 'teleporter': [201, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northeast Stairs', 'id': 370, 'area': 87, 'coordinates': [22, 40], 'teleporter': [202, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northwest Stairs', 'id': 371, 'area': 87, 'coordinates': [9, 40], 'teleporter': [203, 0]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 1', 'id': 372, 'area': 87, 'coordinates': [15, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 2', 'id': 373, 'area': 87, 'coordinates': [16, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 6F - West Stairs', 'id': 374, 'area': 88, 'coordinates': [41, 47], 'teleporter': [204, 0]}, {'name': 'Pazuzu 6F - Northwest Stairs', 'id': 375, 'area': 88, 'coordinates': [41, 40], 'teleporter': [205, 0]}, {'name': 'Pazuzu 6F - Northeast Stairs', 'id': 376, 'area': 88, 'coordinates': [54, 40], 'teleporter': [206, 0]}, {'name': 'Pazuzu 6F - South Stairs', 'id': 377, 'area': 88, 'coordinates': [52, 56], 'teleporter': [207, 0]}, {'name': 'Pazuzu 6F - Pazuzu Script 1', 'id': 378, 'area': 88, 'coordinates': [47, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 6F - Pazuzu Script 2', 'id': 379, 'area': 88, 'coordinates': [48, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 7F Main Room - Southwest Stairs', 'id': 380, 'area': 89, 'coordinates': [15, 54], 'teleporter': [26, 0]}, {'name': 'Pazuzu 7F Main Room - Northeast Stairs', 'id': 381, 'area': 89, 'coordinates': [21, 40], 'teleporter': [27, 0]}, {'name': 'Pazuzu 7F Main Room - Southeast Stairs', 'id': 382, 'area': 89, 'coordinates': [21, 56], 'teleporter': [28, 0]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 1', 'id': 383, 'area': 89, 'coordinates': [15, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 2', 'id': 384, 'area': 89, 'coordinates': [16, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Crystal Script', 'id': 480, 'area': 89, 'coordinates': [15, 40], 'teleporter': [38, 8]}, {'name': 'Pazuzu 1F to 3F - South Stairs', 'id': 385, 'area': 90, 'coordinates': [43, 60], 'teleporter': [29, 0]}, {'name': 'Pazuzu 1F to 3F - North Stairs', 'id': 386, 'area': 90, 'coordinates': [43, 36], 'teleporter': [30, 0]}, {'name': 'Pazuzu 3F to 5F - South Stairs', 'id': 387, 'area': 91, 'coordinates': [43, 60], 'teleporter': [40, 0]}, {'name': 'Pazuzu 3F to 5F - North Stairs', 'id': 388, 'area': 91, 'coordinates': [43, 36], 'teleporter': [41, 0]}, {'name': 'Pazuzu 5F to 7F - South Stairs', 'id': 389, 'area': 92, 'coordinates': [43, 60], 'teleporter': [38, 0]}, {'name': 'Pazuzu 5F to 7F - North Stairs', 'id': 390, 'area': 92, 'coordinates': [43, 36], 'teleporter': [39, 0]}, {'name': 'Pazuzu 2F to 4F - South Stairs', 'id': 391, 'area': 93, 'coordinates': [43, 60], 'teleporter': [21, 0]}, {'name': 'Pazuzu 2F to 4F - North Stairs', 'id': 392, 'area': 93, 'coordinates': [43, 36], 'teleporter': [22, 0]}, {'name': 'Pazuzu 4F to 6F - South Stairs', 'id': 393, 'area': 94, 'coordinates': [43, 60], 'teleporter': [2, 0]}, {'name': 'Pazuzu 4F to 6F - North Stairs', 'id': 394, 'area': 94, 'coordinates': [43, 36], 'teleporter': [3, 0]}, {'name': 'Light Temple - Entrance', 'id': 395, 'area': 95, 'coordinates': [28, 57], 'teleporter': [19, 6]}, {'name': 'Light Temple - Mobius Teleporter Script', 'id': 396, 'area': 95, 'coordinates': [29, 37], 'teleporter': [70, 8]}, {'name': 'Light Temple - Visit Quest Script 1', 'id': 492, 'area': 95, 'coordinates': [34, 39], 'teleporter': [100, 8]}, {'name': 'Light Temple - Visit Quest Script 2', 'id': 493, 'area': 95, 'coordinates': [35, 39], 'teleporter': [100, 8]}, {'name': 'Ship Dock - Mobius Teleporter Script', 'id': 397, 'area': 96, 'coordinates': [15, 18], 'teleporter': [61, 8]}, {'name': 'Ship Dock - From Overworld', 'id': 398, 'area': 96, 'coordinates': [15, 11], 'teleporter': [73, 0]}, {'name': 'Ship Dock - Entrance', 'id': 399, 'area': 96, 'coordinates': [15, 23], 'teleporter': [17, 6]}, {'name': 'Mac Ship Deck - East Entrance Script', 'id': 400, 'area': 97, 'coordinates': [26, 40], 'teleporter': [37, 8]}, {'name': 'Mac Ship Deck - Central Stairs Script', 'id': 401, 'area': 97, 'coordinates': [16, 47], 'teleporter': [50, 8]}, {'name': 'Mac Ship Deck - West Stairs Script', 'id': 402, 'area': 97, 'coordinates': [8, 34], 'teleporter': [51, 8]}, {'name': 'Mac Ship Deck - East Stairs Script', 'id': 403, 'area': 97, 'coordinates': [24, 36], 'teleporter': [52, 8]}, {'name': 'Mac Ship Deck - North Stairs Script', 'id': 404, 'area': 97, 'coordinates': [12, 9], 'teleporter': [53, 8]}, {'name': 'Mac Ship B1 Outer Ring - South Stairs', 'id': 405, 'area': 98, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring - West Stairs', 'id': 406, 'area': 98, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring - East Stairs', 'id': 407, 'area': 98, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Outer Ring - Northwest Stairs', 'id': 408, 'area': 98, 'coordinates': [10, 23], 'teleporter': [88, 0]}, {'name': 'Mac Ship B1 Square Room - North Stairs', 'id': 409, 'area': 98, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room - South Stairs', 'id': 410, 'area': 98, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room - Stairs', 'id': 411, 'area': 98, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor - South Stairs', 'id': 412, 'area': 98, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor - North Stairs', 'id': 413, 'area': 98, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B2 South Corridor - South Stairs', 'id': 414, 'area': 99, 'coordinates': [48, 51], 'teleporter': [57, 1]}, {'name': 'Mac Ship B2 South Corridor - North Stairs Script', 'id': 415, 'area': 99, 'coordinates': [48, 38], 'teleporter': [55, 8]}, {'name': 'Mac Ship B2 North Corridor - South Stairs Script', 'id': 416, 'area': 99, 'coordinates': [48, 27], 'teleporter': [56, 8]}, {'name': 'Mac Ship B2 North Corridor - North Stairs Script', 'id': 417, 'area': 99, 'coordinates': [48, 12], 'teleporter': [57, 8]}, {'name': 'Mac Ship B2 Outer Ring - Northwest Stairs Script', 'id': 418, 'area': 99, 'coordinates': [55, 11], 'teleporter': [58, 8]}, {'name': 'Mac Ship B1 Outer Ring Cleared - South Stairs', 'id': 419, 'area': 100, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - West Stairs', 'id': 420, 'area': 100, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - East Stairs', 'id': 421, 'area': 100, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - North Stairs', 'id': 422, 'area': 100, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - South Stairs', 'id': 423, 'area': 100, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room Cleared - Main Stairs', 'id': 424, 'area': 100, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - South Stairs', 'id': 425, 'area': 100, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - North Stairs', 'id': 426, 'area': 100, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - Northwest Stairs', 'id': 427, 'area': 100, 'coordinates': [23, 10], 'teleporter': [88, 0]}, {'name': 'Doom Castle Corridor of Destiny - South Entrance', 'id': 428, 'area': 101, 'coordinates': [59, 29], 'teleporter': [84, 0]}, {'name': 'Doom Castle Corridor of Destiny - Ice Floor Entrance', 'id': 429, 'area': 101, 'coordinates': [59, 21], 'teleporter': [35, 2]}, {'name': 'Doom Castle Corridor of Destiny - Lava Floor Entrance', 'id': 430, 'area': 101, 'coordinates': [59, 13], 'teleporter': [209, 0]}, {'name': 'Doom Castle Corridor of Destiny - Sky Floor Entrance', 'id': 431, 'area': 101, 'coordinates': [59, 5], 'teleporter': [211, 0]}, {'name': 'Doom Castle Corridor of Destiny - Hero Room Entrance', 'id': 432, 'area': 101, 'coordinates': [59, 61], 'teleporter': [13, 2]}, {'name': 'Doom Castle Ice Floor - Entrance', 'id': 433, 'area': 102, 'coordinates': [23, 42], 'teleporter': [109, 3]}, {'name': 'Doom Castle Lava Floor - Entrance', 'id': 434, 'area': 103, 'coordinates': [23, 40], 'teleporter': [210, 0]}, {'name': 'Doom Castle Sky Floor - Entrance', 'id': 435, 'area': 104, 'coordinates': [24, 41], 'teleporter': [212, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 1', 'id': 436, 'area': 106, 'coordinates': [15, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 2', 'id': 437, 'area': 106, 'coordinates': [16, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 3', 'id': 438, 'area': 106, 'coordinates': [15, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 4', 'id': 439, 'area': 106, 'coordinates': [16, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Hero Statue Script', 'id': 440, 'area': 106, 'coordinates': [15, 17], 'teleporter': [24, 8]}, {'name': 'Doom Castle Hero Room - Entrance', 'id': 441, 'area': 106, 'coordinates': [15, 24], 'teleporter': [110, 3]}, {'name': 'Doom Castle Dark King Room - Entrance', 'id': 442, 'area': 107, 'coordinates': [14, 26], 'teleporter': [52, 0]}, {'name': 'Doom Castle Dark King Room - Dark King Script', 'id': 443, 'area': 107, 'coordinates': [14, 15], 'teleporter': [25, 8]}, {'name': 'Doom Castle Dark King Room - Unknown', 'id': 444, 'area': 107, 'coordinates': [47, 54], 'teleporter': [77, 0]}, {'name': 'Overworld - Level Forest', 'id': 445, 'area': 0, 'type': 'Overworld', 'teleporter': [46, 8]}, {'name': 'Overworld - Foresta', 'id': 446, 'area': 0, 'type': 'Overworld', 'teleporter': [2, 1]}, {'name': 'Overworld - Sand Temple', 'id': 447, 'area': 0, 'type': 'Overworld', 'teleporter': [3, 1]}, {'name': 'Overworld - Bone Dungeon', 'id': 448, 'area': 0, 'type': 'Overworld', 'teleporter': [4, 1]}, {'name': 'Overworld - Focus Tower Foresta', 'id': 449, 'area': 0, 'type': 'Overworld', 'teleporter': [5, 1]}, {'name': 'Overworld - Focus Tower Aquaria', 'id': 450, 'area': 0, 'type': 'Overworld', 'teleporter': [19, 1]}, {'name': 'Overworld - Libra Temple', 'id': 451, 'area': 0, 'type': 'Overworld', 'teleporter': [7, 1]}, {'name': 'Overworld - Aquaria', 'id': 452, 'area': 0, 'type': 'Overworld', 'teleporter': [8, 8]}, {'name': 'Overworld - Wintry Cave', 'id': 453, 'area': 0, 'type': 'Overworld', 'teleporter': [10, 1]}, {'name': 'Overworld - Life Temple', 'id': 454, 'area': 0, 'type': 'Overworld', 'teleporter': [11, 1]}, {'name': 'Overworld - Falls Basin', 'id': 455, 'area': 0, 'type': 'Overworld', 'teleporter': [12, 1]}, {'name': 'Overworld - Ice Pyramid', 'id': 456, 'area': 0, 'type': 'Overworld', 'teleporter': [13, 1]}, {'name': "Overworld - Spencer's Place", 'id': 457, 'area': 0, 'type': 'Overworld', 'teleporter': [48, 8]}, {'name': 'Overworld - Wintry Temple', 'id': 458, 'area': 0, 'type': 'Overworld', 'teleporter': [16, 1]}, {'name': 'Overworld - Focus Tower Frozen Strip', 'id': 459, 'area': 0, 'type': 'Overworld', 'teleporter': [17, 1]}, {'name': 'Overworld - Focus Tower Fireburg', 'id': 460, 'area': 0, 'type': 'Overworld', 'teleporter': [18, 1]}, {'name': 'Overworld - Fireburg', 'id': 461, 'area': 0, 'type': 'Overworld', 'teleporter': [20, 1]}, {'name': 'Overworld - Mine', 'id': 462, 'area': 0, 'type': 'Overworld', 'teleporter': [21, 1]}, {'name': 'Overworld - Sealed Temple', 'id': 463, 'area': 0, 'type': 'Overworld', 'teleporter': [22, 1]}, {'name': 'Overworld - Volcano', 'id': 464, 'area': 0, 'type': 'Overworld', 'teleporter': [23, 1]}, {'name': 'Overworld - Lava Dome', 'id': 465, 'area': 0, 'type': 'Overworld', 'teleporter': [24, 1]}, {'name': 'Overworld - Focus Tower Windia', 'id': 466, 'area': 0, 'type': 'Overworld', 'teleporter': [6, 1]}, {'name': 'Overworld - Rope Bridge', 'id': 467, 'area': 0, 'type': 'Overworld', 'teleporter': [25, 1]}, {'name': 'Overworld - Alive Forest', 'id': 468, 'area': 0, 'type': 'Overworld', 'teleporter': [26, 1]}, {'name': 'Overworld - Giant Tree', 'id': 469, 'area': 0, 'type': 'Overworld', 'teleporter': [27, 1]}, {'name': 'Overworld - Kaidge Temple', 'id': 470, 'area': 0, 'type': 'Overworld', 'teleporter': [28, 1]}, {'name': 'Overworld - Windia', 'id': 471, 'area': 0, 'type': 'Overworld', 'teleporter': [29, 1]}, {'name': 'Overworld - Windhole Temple', 'id': 472, 'area': 0, 'type': 'Overworld', 'teleporter': [30, 1]}, {'name': 'Overworld - Mount Gale', 'id': 473, 'area': 0, 'type': 'Overworld', 'teleporter': [31, 1]}, {'name': 'Overworld - Pazuzu Tower', 'id': 474, 'area': 0, 'type': 'Overworld', 'teleporter': [32, 1]}, {'name': 'Overworld - Ship Dock', 'id': 475, 'area': 0, 'type': 'Overworld', 'teleporter': [62, 1]}, {'name': 'Overworld - Doom Castle', 'id': 476, 'area': 0, 'type': 'Overworld', 'teleporter': [33, 1]}, {'name': 'Overworld - Light Temple', 'id': 477, 'area': 0, 'type': 'Overworld', 'teleporter': [34, 1]}, {'name': 'Overworld - Mac Ship', 'id': 478, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Overworld - Mac Ship Doom', 'id': 479, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Dummy House - Bed Script', 'id': 480, 'area': 17, 'coordinates': [40, 56], 'teleporter': [1, 8]}, {'name': 'Dummy House - Entrance', 'id': 481, 'area': 17, 'coordinates': [41, 59], 'teleporter': [0, 10]}] \ No newline at end of file diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml deleted file mode 100644 index e0c2e8d7f9fc..000000000000 --- a/worlds/ffmq/data/rooms.yaml +++ /dev/null @@ -1,4026 +0,0 @@ -- name: Overworld - id: 0 - type: "Overworld" - game_objects: [] - links: - - target_room: 220 # To Forest Subregion - access: [] -- name: Subregion Foresta - id: 220 - type: "Subregion" - region: "Foresta" - game_objects: - - name: "Foresta South Battlefield" - object_id: 0x01 - location: "ForestaSouthBattlefield" - location_slot: "ForestaSouthBattlefield" - type: "BattlefieldXp" - access: [] - - name: "Foresta West Battlefield" - object_id: 0x02 - location: "ForestaWestBattlefield" - location_slot: "ForestaWestBattlefield" - type: "BattlefieldItem" - access: [] - - name: "Foresta East Battlefield" - object_id: 0x03 - location: "ForestaEastBattlefield" - location_slot: "ForestaEastBattlefield" - type: "BattlefieldGp" - access: [] - links: - - target_room: 15 # Level Forest - location: "LevelForest" - location_slot: "LevelForest" - entrance: 445 - teleporter: [0x2E, 8] - access: [] - - target_room: 16 # Foresta - location: "Foresta" - location_slot: "Foresta" - entrance: 446 - teleporter: [0x02, 1] - access: [] - - target_room: 24 # Sand Temple - location: "SandTemple" - location_slot: "SandTemple" - entrance: 447 - teleporter: [0x03, 1] - access: [] - - target_room: 25 # Bone Dungeon - location: "BoneDungeon" - location_slot: "BoneDungeon" - entrance: 448 - teleporter: [0x04, 1] - access: [] - - target_room: 3 # Focus Tower Foresta - location: "FocusTowerForesta" - location_slot: "FocusTowerForesta" - entrance: 449 - teleporter: [0x05, 1] - access: [] - - target_room: 221 - access: ["SandCoin"] - - target_room: 224 - access: ["RiverCoin"] - - target_room: 226 - access: ["SunCoin"] -- name: Subregion Aquaria - id: 221 - type: "Subregion" - region: "Aquaria" - game_objects: - - name: "South of Libra Temple Battlefield" - object_id: 0x04 - location: "AquariaBattlefield01" - location_slot: "AquariaBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "East of Libra Temple Battlefield" - object_id: 0x05 - location: "AquariaBattlefield02" - location_slot: "AquariaBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "South of Aquaria Battlefield" - object_id: 0x06 - location: "AquariaBattlefield03" - location_slot: "AquariaBattlefield03" - type: "BattlefieldItem" - access: [] - - name: "South of Wintry Cave Battlefield" - object_id: 0x07 - location: "WintryBattlefield01" - location_slot: "WintryBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "West of Wintry Cave Battlefield" - object_id: 0x08 - location: "WintryBattlefield02" - location_slot: "WintryBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "Ice Pyramid Battlefield" - object_id: 0x09 - location: "PyramidBattlefield01" - location_slot: "PyramidBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 10 # Focus Tower Aquaria - location: "FocusTowerAquaria" - location_slot: "FocusTowerAquaria" - entrance: 450 - teleporter: [0x13, 1] - access: [] - - target_room: 39 # Libra Temple - location: "LibraTemple" - location_slot: "LibraTemple" - entrance: 451 - teleporter: [0x07, 1] - access: [] - - target_room: 40 # Aquaria - location: "Aquaria" - location_slot: "Aquaria" - entrance: 452 - teleporter: [0x08, 8] - access: [] - - target_room: 45 # Wintry Cave - location: "WintryCave" - location_slot: "WintryCave" - entrance: 453 - teleporter: [0x0A, 1] - access: [] - - target_room: 52 # Falls Basin - location: "FallsBasin" - location_slot: "FallsBasin" - entrance: 455 - teleporter: [0x0C, 1] - access: [] - - target_room: 54 # Ice Pyramid - location: "IcePyramid" - location_slot: "IcePyramid" - entrance: 456 - teleporter: [0x0D, 1] # Will be switched to a script - access: [] - - target_room: 220 - access: ["SandCoin"] - - target_room: 224 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["SandCoin", "SunCoin"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Life Temple - id: 222 - type: "Subregion" - region: "LifeTemple" - game_objects: [] - links: - - target_room: 51 # Life Temple - location: "LifeTemple" - location_slot: "LifeTemple" - entrance: 454 - teleporter: [0x0B, 1] - access: [] -- name: Subregion Frozen Fields - id: 223 - type: "Subregion" - region: "AquariaFrozenField" - game_objects: - - name: "North of Libra Temple Battlefield" - object_id: 0x0A - location: "LibraBattlefield01" - location_slot: "LibraBattlefield01" - type: "BattlefieldItem" - access: [] - - name: "Aquaria Frozen Field Battlefield" - object_id: 0x0B - location: "LibraBattlefield02" - location_slot: "LibraBattlefield02" - type: "BattlefieldXp" - access: [] - links: - - target_room: 74 # Wintry Temple - location: "WintryTemple" - location_slot: "WintryTemple" - entrance: 458 - teleporter: [0x10, 1] - access: [] - - target_room: 14 # Focus Tower Frozen Strip - location: "FocusTowerFrozen" - location_slot: "FocusTowerFrozen" - entrance: 459 - teleporter: [0x11, 1] - access: [] - - target_room: 221 - access: [] - - target_room: 225 - access: ["SummerAquaria", "DualheadHydra"] -- name: Subregion Fireburg - id: 224 - type: "Subregion" - region: "Fireburg" - game_objects: - - name: "Path to Fireburg Southern Battlefield" - object_id: 0x0C - location: "FireburgBattlefield01" - location_slot: "FireburgBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Path to Fireburg Central Battlefield" - object_id: 0x0D - location: "FireburgBattlefield02" - location_slot: "FireburgBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Path to Fireburg Northern Battlefield" - object_id: 0x0E - location: "FireburgBattlefield03" - location_slot: "FireburgBattlefield03" - type: "BattlefieldXp" - access: [] - - name: "Sealed Temple Battlefield" - object_id: 0x0F - location: "MineBattlefield01" - location_slot: "MineBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Mine Battlefield" - object_id: 0x10 - location: "MineBattlefield02" - location_slot: "MineBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Boulder Battlefield" - object_id: 0x11 - location: "MineBattlefield03" - location_slot: "MineBattlefield03" - type: "BattlefieldXp" - access: [] - links: - - target_room: 13 # Focus Tower Fireburg - location: "FocusTowerFireburg" - location_slot: "FocusTowerFireburg" - entrance: 460 - teleporter: [0x12, 1] - access: [] - - target_room: 76 # Fireburg - location: "Fireburg" - location_slot: "Fireburg" - entrance: 461 - teleporter: [0x14, 1] - access: [] - - target_room: 84 # Mine - location: "Mine" - location_slot: "Mine" - entrance: 462 - teleporter: [0x15, 1] - access: [] - - target_room: 92 # Sealed Temple - location: "SealedTemple" - location_slot: "SealedTemple" - entrance: 463 - teleporter: [0x16, 1] - access: [] - - target_room: 93 # Volcano - location: "Volcano" - location_slot: "Volcano" - entrance: 464 - teleporter: [0x17, 1] # Also this one / 0x0F, 8 - access: [] - - target_room: 100 # Lava Dome - location: "LavaDome" - location_slot: "LavaDome" - entrance: 465 - teleporter: [0x18, 1] - access: [] - - target_room: 220 - access: ["RiverCoin"] - - target_room: 221 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["RiverCoin", "SunCoin"] - - target_room: 225 - access: ["DualheadHydra"] -- name: Subregion Volcano Battlefield - id: 225 - type: "Subregion" - region: "VolcanoBattlefield" - game_objects: - - name: "Volcano Battlefield" - object_id: 0x12 - location: "VolcanoBattlefield01" - location_slot: "VolcanoBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 224 - access: ["DualheadHydra"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Windia - id: 226 - type: "Subregion" - region: "Windia" - game_objects: - - name: "Kaidge Temple Battlefield" - object_id: 0x13 - location: "WindiaBattlefield01" - location_slot: "WindiaBattlefield01" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - - name: "South of Windia Battlefield" - object_id: 0x14 - location: "WindiaBattlefield02" - location_slot: "WindiaBattlefield02" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - links: - - target_room: 9 # Focus Tower Windia - location: "FocusTowerWindia" - location_slot: "FocusTowerWindia" - entrance: 466 - teleporter: [0x06, 1] - access: [] - - target_room: 123 # Rope Bridge - location: "RopeBridge" - location_slot: "RopeBridge" - entrance: 467 - teleporter: [0x19, 1] - access: [] - - target_room: 124 # Alive Forest - location: "AliveForest" - location_slot: "AliveForest" - entrance: 468 - teleporter: [0x1A, 1] - access: [] - - target_room: 125 # Giant Tree - location: "GiantTree" - location_slot: "GiantTree" - entrance: 469 - teleporter: [0x1B, 1] - access: ["Barred"] - - target_room: 152 # Kaidge Temple - location: "KaidgeTemple" - location_slot: "KaidgeTemple" - entrance: 470 - teleporter: [0x1C, 1] - access: [] - - target_room: 156 # Windia - location: "Windia" - location_slot: "Windia" - entrance: 471 - teleporter: [0x1D, 1] - access: [] - - target_room: 154 # Windhole Temple - location: "WindholeTemple" - location_slot: "WindholeTemple" - entrance: 472 - teleporter: [0x1E, 1] - access: [] - - target_room: 155 # Mount Gale - location: "MountGale" - location_slot: "MountGale" - entrance: 473 - teleporter: [0x1F, 1] - access: [] - - target_room: 166 # Pazuzu Tower - location: "PazuzusTower" - location_slot: "PazuzusTower" - entrance: 474 - teleporter: [0x20, 1] - access: [] - - target_room: 220 - access: ["SunCoin"] - - target_room: 221 - access: ["SandCoin", "SunCoin"] - - target_room: 224 - access: ["RiverCoin", "SunCoin"] - - target_room: 227 - access: ["RainbowBridge"] -- name: Subregion Spencer's Cave - id: 227 - type: "Subregion" - region: "SpencerCave" - game_objects: [] - links: - - target_room: 73 # Spencer's Place - location: "SpencersPlace" - location_slot: "SpencersPlace" - entrance: 457 - teleporter: [0x30, 8] - access: [] - - target_room: 226 - access: ["RainbowBridge"] -- name: Subregion Ship Dock - id: 228 - type: "Subregion" - region: "ShipDock" - game_objects: [] - links: - - target_room: 186 # Ship Dock - location: "ShipDock" - location_slot: "ShipDock" - entrance: 475 - teleporter: [0x3E, 1] - access: [] - - target_room: 229 - access: ["ShipLiberated", "ShipDockAccess"] -- name: Subregion Mac's Ship - id: 229 - type: "Subregion" - region: "MacShip" - game_objects: [] - links: - - target_room: 187 # Mac Ship - location: "MacsShip" - location_slot: "MacsShip" - entrance: 478 - teleporter: [0x24, 1] - access: [] - - target_room: 228 - access: ["ShipLiberated", "ShipDockAccess"] - - target_room: 231 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Subregion Light Temple - id: 230 - type: "Subregion" - region: "LightTemple" - game_objects: [] - links: - - target_room: 185 # Light Temple - location: "LightTemple" - location_slot: "LightTemple" - entrance: 477 - teleporter: [0x23, 1] - access: [] -- name: Subregion Doom Castle - id: 231 - type: "Subregion" - region: "DoomCastle" - game_objects: [] - links: - - target_room: 1 # Doom Castle - location: "DoomCastle" - location_slot: "DoomCastle" - entrance: 476 - teleporter: [0x21, 1] - access: [] - - target_room: 187 # Mac Ship Doom - location: "MacsShipDoom" - location_slot: "MacsShipDoom" - entrance: 479 - teleporter: [0x24, 1] - access: ["Barred"] - - target_room: 229 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Doom Castle - Sand Floor - id: 1 - game_objects: - - name: "Doom Castle B2 - Southeast Chest" - object_id: 0x01 - type: "Chest" - access: ["Bomb"] - - name: "Doom Castle B2 - Bone Ledge Box" - object_id: 0x1E - type: "Box" - access: [] - - name: "Doom Castle B2 - Hook Platform Box" - object_id: 0x1F - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 231 - entrance: 1 - teleporter: [1, 6] - access: [] - - target_room: 5 - entrance: 0 - teleporter: [0, 0] - access: ["DragonClaw", "MegaGrenade"] -- name: Doom Castle - Aero Room - id: 2 - game_objects: - - name: "Doom Castle B2 - Sun Door Chest" - object_id: 0x00 - type: "Chest" - access: [] - links: - - target_room: 4 - entrance: 2 - teleporter: [1, 0] - access: [] -- name: Focus Tower B1 - Main Loop - id: 3 - game_objects: [] - links: - - target_room: 220 - entrance: 3 - teleporter: [2, 6] - access: [] - - target_room: 6 - entrance: 4 - teleporter: [4, 0] - access: [] -- name: Focus Tower B1 - Aero Corridor - id: 4 - game_objects: [] - links: - - target_room: 9 - entrance: 5 - teleporter: [5, 0] - access: [] - - target_room: 2 - entrance: 6 - teleporter: [8, 0] - access: [] -- name: Focus Tower B1 - Inner Loop - id: 5 - game_objects: [] - links: - - target_room: 1 - entrance: 8 - teleporter: [7, 0] - access: [] - - target_room: 201 - entrance: 7 - teleporter: [6, 0] - access: [] -- name: Focus Tower 1F Main Lobby - id: 6 - game_objects: - - name: "Focus Tower 1F - Main Lobby Box" - object_id: 0x21 - type: "Box" - access: [] - links: - - target_room: 3 - entrance: 11 - teleporter: [11, 0] - access: [] - - target_room: 7 - access: ["SandCoin"] - - target_room: 8 - access: ["RiverCoin"] - - target_room: 9 - access: ["SunCoin"] -- name: Focus Tower 1F SandCoin Room - id: 7 - game_objects: [] - links: - - target_room: 6 - access: ["SandCoin"] - - target_room: 10 - entrance: 10 - teleporter: [10, 0] - access: [] -- name: Focus Tower 1F RiverCoin Room - id: 8 - game_objects: [] - links: - - target_room: 6 - access: ["RiverCoin"] - - target_room: 11 - entrance: 14 - teleporter: [14, 0] - access: [] -- name: Focus Tower 1F SunCoin Room - id: 9 - game_objects: [] - links: - - target_room: 6 - access: ["SunCoin"] - - target_room: 4 - entrance: 12 - teleporter: [12, 0] - access: [] - - target_room: 226 - entrance: 9 - teleporter: [3, 6] - access: [] -- name: Focus Tower 1F SkyCoin Room - id: 201 - game_objects: [] - links: - - target_room: 195 - entrance: 13 - teleporter: [13, 0] - access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] - - target_room: 5 - entrance: 15 - teleporter: [15, 0] - access: [] -- name: Focus Tower 2F - Sand Coin Passage - id: 10 - game_objects: - - name: "Focus Tower 2F - Sand Door Chest" - object_id: 0x03 - type: "Chest" - access: [] - links: - - target_room: 221 - entrance: 16 - teleporter: [4, 6] - access: [] - - target_room: 7 - entrance: 17 - teleporter: [17, 0] - access: [] -- name: Focus Tower 2F - River Coin Passage - id: 11 - game_objects: [] - links: - - target_room: 8 - entrance: 18 - teleporter: [18, 0] - access: [] - - target_room: 13 - entrance: 19 - teleporter: [20, 0] - access: [] -- name: Focus Tower 2F - Venus Chest Room - id: 12 - game_objects: - - name: "Focus Tower 2F - Back Door Chest" - object_id: 0x02 - type: "Chest" - access: [] - - name: "Focus Tower 2F - Venus Chest" - object_id: 9 - type: "NPC" - access: ["Bomb", "VenusKey"] - links: - - target_room: 14 - entrance: 20 - teleporter: [19, 0] - access: [] -- name: Focus Tower 3F - Lower Floor - id: 13 - game_objects: - - name: "Focus Tower 3F - River Door Box" - object_id: 0x22 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 22 - teleporter: [6, 6] - access: [] - - target_room: 11 - entrance: 23 - teleporter: [24, 0] - access: [] -- name: Focus Tower 3F - Upper Floor - id: 14 - game_objects: [] - links: - - target_room: 223 - entrance: 24 - teleporter: [5, 6] - access: [] - - target_room: 12 - entrance: 25 - teleporter: [23, 0] - access: [] -- name: Level Forest - id: 15 - game_objects: - - name: "Level Forest - Northwest Box" - object_id: 0x28 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Northeast Box" - object_id: 0x29 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Middle Box" - object_id: 0x2A - type: "Box" - access: [] - - name: "Level Forest - Southwest Box" - object_id: 0x2B - type: "Box" - access: ["Axe"] - - name: "Level Forest - Southeast Box" - object_id: 0x2C - type: "Box" - access: ["Axe"] - - name: "Minotaur" - object_id: 0 - type: "Trigger" - on_trigger: ["Minotaur"] - access: ["Kaeli1"] - - name: "Level Forest - Old Man" - object_id: 0 - type: "NPC" - access: [] - - name: "Level Forest - Kaeli" - object_id: 1 - type: "NPC" - access: ["Kaeli1", "Minotaur"] - links: - - target_room: 220 - entrance: 28 - teleporter: [25, 0] - access: [] -- name: Foresta - id: 16 - game_objects: - - name: "Foresta - Outside Box" - object_id: 0x2D - type: "Box" - access: ["Axe"] - links: - - target_room: 220 - entrance: 38 - teleporter: [31, 0] - access: [] - - target_room: 17 - entrance: 44 - teleporter: [0, 5] - access: [] - - target_room: 18 - entrance: 42 - teleporter: [32, 4] - access: [] - - target_room: 19 - entrance: 43 - teleporter: [33, 0] - access: [] - - target_room: 20 - entrance: 45 - teleporter: [1, 5] - access: [] -- name: Kaeli's House - id: 17 - game_objects: - - name: "Foresta - Kaeli's House Box" - object_id: 0x2E - type: "Box" - access: [] - - name: "Kaeli Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli1"] - access: ["TreeWither"] - - name: "Kaeli 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli2"] - access: ["Kaeli1", "Minotaur", "Elixir"] - links: - - target_room: 16 - entrance: 46 - teleporter: [86, 3] - access: [] -- name: Foresta Houses - Old Man's House Main - id: 18 - game_objects: [] - links: - - target_room: 19 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 47 - teleporter: [34, 0] - access: [] -- name: Foresta Houses - Old Man's House Back - id: 19 - game_objects: - - name: "Foresta - Old Man House Chest" - object_id: 0x05 - type: "Chest" - access: [] - - name: "Old Man Barrel" - object_id: 0 - type: "Trigger" - on_trigger: ["BarrelPushed"] - access: [] - links: - - target_room: 18 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 48 - teleporter: [35, 0] - access: [] -- name: Foresta Houses - Rest House - id: 20 - game_objects: - - name: "Foresta - Rest House Box" - object_id: 0x2F - type: "Box" - access: [] - links: - - target_room: 16 - entrance: 50 - teleporter: [87, 3] - access: [] -- name: Libra Treehouse - id: 21 - game_objects: - - name: "Alive Forest - Libra Treehouse Box" - object_id: 0x32 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 51 - teleporter: [67, 8] - access: ["LibraCrest"] -- name: Gemini Treehouse - id: 22 - game_objects: - - name: "Alive Forest - Gemini Treehouse Box" - object_id: 0x33 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 52 - teleporter: [68, 8] - access: ["GeminiCrest"] -- name: Mobius Treehouse - id: 23 - game_objects: - - name: "Alive Forest - Mobius Treehouse West Box" - object_id: 0x30 - type: "Box" - access: [] - - name: "Alive Forest - Mobius Treehouse East Box" - object_id: 0x31 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 53 - teleporter: [69, 8] - access: ["MobiusCrest"] -- name: Sand Temple - id: 24 - game_objects: - - name: "Tristam Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Tristam"] - access: [] - links: - - target_room: 220 - entrance: 54 - teleporter: [36, 0] - access: [] -- name: Bone Dungeon 1F - id: 25 - game_objects: - - name: "Bone Dungeon 1F - Entrance Room West Box" - object_id: 0x35 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room Middle Box" - object_id: 0x36 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room East Box" - object_id: 0x37 - type: "Box" - access: [] - links: - - target_room: 220 - entrance: 55 - teleporter: [37, 0] - access: [] - - target_room: 26 - entrance: 56 - teleporter: [2, 2] - access: [] -- name: Bone Dungeon B1 - Waterway - id: 26 - game_objects: - - name: "Bone Dungeon B1 - Skull Chest" - object_id: 0x06 - type: "Chest" - access: ["Bomb"] - - name: "Bone Dungeon B1 - Tristam" - object_id: 2 - type: "NPC" - access: ["Tristam"] - - name: "Tristam Bone Dungeon Item Given" - object_id: 0 - type: "Trigger" - on_trigger: ["TristamBoneItemGiven"] - access: ["Tristam"] - links: - - target_room: 25 - entrance: 59 - teleporter: [88, 3] - access: [] - - target_room: 28 - entrance: 57 - teleporter: [3, 2] - access: ["Bomb"] -- name: Bone Dungeon B1 - Checker Room - id: 28 - game_objects: - - name: "Bone Dungeon B1 - Checker Room Box" - object_id: 0x38 - type: "Box" - access: ["Bomb"] - links: - - target_room: 26 - entrance: 61 - teleporter: [89, 3] - access: [] - - target_room: 30 - entrance: 60 - teleporter: [4, 2] - access: [] -- name: Bone Dungeon B1 - Hidden Room - id: 29 - game_objects: - - name: "Bone Dungeon B1 - Ribcage Waterway Box" - object_id: 0x39 - type: "Box" - access: [] - links: - - target_room: 31 - entrance: 62 - teleporter: [91, 3] - access: [] -- name: Bone Dungeon B2 - Exploding Skull Room - First Room - id: 30 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Alcove Box" - object_id: 0x3B - type: "Box" - access: [] - - name: "Long Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["LongSpineBombed"] - access: ["Bomb"] - links: - - target_room: 28 - entrance: 65 - teleporter: [90, 3] - access: [] - - target_room: 31 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Second Room - id: 31 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" - object_id: 0x3A - type: "Box" - access: [] - - name: "Short Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["ShortSpineBombed"] - access: ["Bomb"] - links: - - target_room: 29 - entrance: 63 - teleporter: [5, 2] - access: ["LongSpineBombed"] - - target_room: 32 - access: ["ShortSpineBombed"] - - target_room: 30 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Third Room - id: 32 - game_objects: [] - links: - - target_room: 35 - entrance: 64 - teleporter: [6, 2] - access: [] - - target_room: 31 - access: ["ShortSpineBombed"] -- name: Bone Dungeon B2 - Box Room - id: 33 - game_objects: - - name: "Bone Dungeon B2 - Lone Room Box" - object_id: 0x3D - type: "Box" - access: [] - links: - - target_room: 36 - entrance: 66 - teleporter: [93, 3] - access: [] -- name: Bone Dungeon B2 - Quake Room - id: 34 - game_objects: - - name: "Bone Dungeon B2 - Penultimate Room Chest" - object_id: 0x07 - type: "Chest" - access: [] - links: - - target_room: 37 - entrance: 67 - teleporter: [94, 3] - access: [] -- name: Bone Dungeon B2 - Two Skulls Room - First Room - id: 35 - game_objects: - - name: "Bone Dungeon B2 - Two Skulls Room Box" - object_id: 0x3C - type: "Box" - access: [] - - name: "Skull 1" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull1Bombed"] - access: ["Bomb"] - links: - - target_room: 32 - entrance: 71 - teleporter: [92, 3] - access: [] - - target_room: 36 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Second Room - id: 36 - game_objects: - - name: "Skull 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull2Bombed"] - access: ["Bomb"] - links: - - target_room: 33 - entrance: 68 - teleporter: [7, 2] - access: [] - - target_room: 37 - access: ["Skull2Bombed"] - - target_room: 35 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Third Room - id: 37 - game_objects: [] - links: - - target_room: 34 - entrance: 69 - teleporter: [8, 2] - access: [] - - target_room: 38 - entrance: 70 - teleporter: [9, 2] - access: ["Bomb"] - - target_room: 36 - access: ["Skull2Bombed"] -- name: Bone Dungeon B2 - Boss Room - id: 38 - game_objects: - - name: "Bone Dungeon B2 - North Box" - object_id: 0x3E - type: "Box" - access: [] - - name: "Bone Dungeon B2 - South Box" - object_id: 0x3F - type: "Box" - access: [] - - name: "Bone Dungeon B2 - Flamerus Rex Chest" - object_id: 0x08 - type: "Chest" - access: [] - - name: "Bone Dungeon B2 - Tristam's Treasure Chest" - object_id: 0x04 - type: "Chest" - access: [] - - name: "Flamerus Rex" - object_id: 0 - type: "Trigger" - on_trigger: ["FlamerusRex"] - access: [] - links: - - target_room: 37 - entrance: 74 - teleporter: [95, 3] - access: [] -- name: Libra Temple - id: 39 - game_objects: - - name: "Libra Temple - Box" - object_id: 0x40 - type: "Box" - access: [] - - name: "Phoebe Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Phoebe1"] - access: [] - links: - - target_room: 221 - entrance: 75 - teleporter: [13, 6] - access: [] - - target_room: 51 - entrance: 76 - teleporter: [59, 8] - access: ["LibraCrest"] -- name: Aquaria - id: 40 - game_objects: - - name: "Summer Aquaria" - object_id: 0 - type: "Trigger" - on_trigger: ["SummerAquaria"] - access: ["WakeWater"] - links: - - target_room: 221 - entrance: 77 - teleporter: [8, 6] - access: [] - - target_room: 41 - entrance: 81 - teleporter: [10, 5] - access: [] - - target_room: 42 - entrance: 82 - teleporter: [44, 4] - access: [] - - target_room: 44 - entrance: 83 - teleporter: [11, 5] - access: [] - - target_room: 71 - entrance: 89 - teleporter: [42, 0] - access: ["SummerAquaria"] - - target_room: 71 - entrance: 90 - teleporter: [43, 0] - access: ["SummerAquaria"] -- name: Phoebe's House - id: 41 - game_objects: - - name: "Aquaria - Phoebe's House Chest" - object_id: 0x41 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 93 - teleporter: [5, 8] - access: [] -- name: Aquaria Vendor House - id: 42 - game_objects: - - name: "Aquaria - Vendor" - object_id: 4 - type: "NPC" - access: [] - - name: "Aquaria - Vendor House Box" - object_id: 0x42 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 94 - teleporter: [40, 8] - access: [] - - target_room: 43 - entrance: 95 - teleporter: [47, 0] - access: [] -- name: Aquaria Gemini Room - id: 43 - game_objects: [] - links: - - target_room: 42 - entrance: 97 - teleporter: [48, 0] - access: [] - - target_room: 81 - entrance: 96 - teleporter: [72, 8] - access: ["GeminiCrest"] -- name: Aquaria INN - id: 44 - game_objects: [] - links: - - target_room: 40 - entrance: 98 - teleporter: [75, 8] - access: [] -- name: Wintry Cave 1F - East Ledge - id: 45 - game_objects: - - name: "Wintry Cave 1F - North Box" - object_id: 0x43 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Entrance Box" - object_id: 0x46 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Slippery Cliff Box" - object_id: 0x44 - type: "Box" - access: ["Claw"] - - name: "Wintry Cave 1F - Phoebe" - object_id: 5 - type: "NPC" - access: ["Phoebe1"] - links: - - target_room: 221 - entrance: 99 - teleporter: [49, 0] - access: [] - - target_room: 49 - entrance: 100 - teleporter: [14, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 1F - Central Space - id: 46 - game_objects: - - name: "Wintry Cave 1F - Scenic Overlook Box" - object_id: 0x45 - type: "Box" - access: ["Claw"] - links: - - target_room: 45 - access: ["Claw"] - - target_room: 47 - access: ["Claw"] -- name: Wintry Cave 1F - West Ledge - id: 47 - game_objects: [] - links: - - target_room: 48 - entrance: 101 - teleporter: [15, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 2F - id: 48 - game_objects: - - name: "Wintry Cave 2F - West Left Box" - object_id: 0x47 - type: "Box" - access: [] - - name: "Wintry Cave 2F - West Right Box" - object_id: 0x48 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Left Box" - object_id: 0x49 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Right Box" - object_id: 0x4A - type: "Box" - access: [] - links: - - target_room: 47 - entrance: 104 - teleporter: [97, 3] - access: [] - - target_room: 50 - entrance: 103 - teleporter: [50, 0] - access: [] -- name: Wintry Cave 3F Top - id: 49 - game_objects: - - name: "Wintry Cave 3F - West Box" - object_id: 0x4B - type: "Box" - access: [] - - name: "Wintry Cave 3F - East Box" - object_id: 0x4C - type: "Box" - access: [] - links: - - target_room: 45 - entrance: 105 - teleporter: [96, 3] - access: [] -- name: Wintry Cave 3F Bottom - id: 50 - game_objects: - - name: "Wintry Cave 3F - Squidite Chest" - object_id: 0x09 - type: "Chest" - access: ["Phanquid"] - - name: "Phanquid" - object_id: 0 - type: "Trigger" - on_trigger: ["Phanquid"] - access: [] - - name: "Wintry Cave 3F - Before Boss Box" - object_id: 0x4D - type: "Box" - access: [] - links: - - target_room: 48 - entrance: 106 - teleporter: [51, 0] - access: [] -- name: Life Temple - id: 51 - game_objects: - - name: "Life Temple - Box" - object_id: 0x4E - type: "Box" - access: [] - - name: "Life Temple - Mysterious Man" - object_id: 6 - type: "NPC" - access: [] - links: - - target_room: 222 - entrance: 107 - teleporter: [14, 6] - access: [] - - target_room: 39 - entrance: 108 - teleporter: [60, 8] - access: ["LibraCrest"] -- name: Fall Basin - id: 52 - game_objects: - - name: "Falls Basin - Snow Crab Chest" - object_id: 0x0A - type: "Chest" - access: ["FreezerCrab"] - - name: "Freezer Crab" - object_id: 0 - type: "Trigger" - on_trigger: ["FreezerCrab"] - access: [] - - name: "Falls Basin - Box" - object_id: 0x4F - type: "Box" - access: [] - links: - - target_room: 221 - entrance: 111 - teleporter: [53, 0] - access: [] -- name: Ice Pyramid B1 Taunt Room - id: 53 - game_objects: - - name: "Ice Pyramid B1 - Chest" - object_id: 0x0B - type: "Chest" - access: [] - - name: "Ice Pyramid B1 - West Box" - object_id: 0x50 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - North Box" - object_id: 0x51 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - East Box" - object_id: 0x52 - type: "Box" - access: [] - links: - - target_room: 68 - entrance: 113 - teleporter: [55, 0] - access: [] -- name: Ice Pyramid 1F Maze Lobby - id: 54 - game_objects: - - name: "Ice Pyramid 1F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid1FStatue"] - access: ["Sword"] - links: - - target_room: 221 - entrance: 114 - teleporter: [56, 0] - access: [] - - target_room: 55 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 1F Maze - id: 55 - game_objects: - - name: "Ice Pyramid 1F - East Alcove Chest" - object_id: 0x0D - type: "Chest" - access: [] - - name: "Ice Pyramid 1F - Sandwiched Alcove Box" - object_id: 0x53 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Left Box" - object_id: 0x54 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Right Box" - object_id: 0x55 - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 116 - teleporter: [57, 0] - access: [] - - target_room: 57 - entrance: 117 - teleporter: [58, 0] - access: [] - - target_room: 58 - entrance: 118 - teleporter: [59, 0] - access: [] - - target_room: 59 - entrance: 119 - teleporter: [60, 0] - access: [] - - target_room: 60 - entrance: 120 - teleporter: [61, 0] - access: [] - - target_room: 54 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 2F South Tiled Room - id: 56 - game_objects: - - name: "Ice Pyramid 2F - South Side Glass Door Box" - object_id: 0x57 - type: "Box" - access: ["Sword"] - - name: "Ice Pyramid 2F - South Side East Box" - object_id: 0x5B - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 122 - teleporter: [62, 0] - access: [] - - target_room: 61 - entrance: 123 - teleporter: [67, 0] - access: [] -- name: Ice Pyramid 2F West Room - id: 57 - game_objects: - - name: "Ice Pyramid 2F - Northwest Room Box" - object_id: 0x5A - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 124 - teleporter: [63, 0] - access: [] -- name: Ice Pyramid 2F Center Room - id: 58 - game_objects: - - name: "Ice Pyramid 2F - Center Room Box" - object_id: 0x56 - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 125 - teleporter: [64, 0] - access: [] -- name: Ice Pyramid 2F Small North Room - id: 59 - game_objects: - - name: "Ice Pyramid 2F - North Room Glass Door Box" - object_id: 0x58 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 126 - teleporter: [65, 0] - access: [] -- name: Ice Pyramid 2F North Corridor - id: 60 - game_objects: - - name: "Ice Pyramid 2F - North Corridor Glass Door Box" - object_id: 0x59 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 127 - teleporter: [66, 0] - access: [] - - target_room: 62 - entrance: 128 - teleporter: [68, 0] - access: [] -- name: Ice Pyramid 3F Two Boxes Room - id: 61 - game_objects: - - name: "Ice Pyramid 3F - Staircase Dead End Left Box" - object_id: 0x5E - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Staircase Dead End Right Box" - object_id: 0x5F - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 129 - teleporter: [69, 0] - access: [] -- name: Ice Pyramid 3F Main Loop - id: 62 - game_objects: - - name: "Ice Pyramid 3F - Inner Room North Box" - object_id: 0x5C - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Inner Room South Box" - object_id: 0x5D - type: "Box" - access: [] - - name: "Ice Pyramid 3F - East Alcove Box" - object_id: 0x60 - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Leapfrog Box" - object_id: 0x61 - type: "Box" - access: [] - - name: "Ice Pyramid 3F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid3FStatue"] - access: ["Sword"] - links: - - target_room: 60 - entrance: 130 - teleporter: [70, 0] - access: [] - - target_room: 63 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 3F Blocked Room - id: 63 - game_objects: [] - links: - - target_room: 64 - entrance: 131 - teleporter: [71, 0] - access: [] - - target_room: 62 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 4F Main Loop - id: 64 - game_objects: [] - links: - - target_room: 66 - entrance: 133 - teleporter: [73, 0] - access: [] - - target_room: 63 - entrance: 132 - teleporter: [72, 0] - access: [] - - target_room: 65 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 4F Treasure Room - id: 65 - game_objects: - - name: "Ice Pyramid 4F - Chest" - object_id: 0x0C - type: "Chest" - access: [] - - name: "Ice Pyramid 4F - Northwest Box" - object_id: 0x62 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Left Box" - object_id: 0x63 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Right Box" - object_id: 0x64 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Left Box" - object_id: 0x65 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Right Box" - object_id: 0x66 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Left Box" - object_id: 0x67 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Right Box" - object_id: 0x68 - type: "Box" - access: [] - - name: "Ice Pyramid 4F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid4FStatue"] - access: ["Sword"] - links: - - target_room: 64 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 5F Leap of Faith Room - id: 66 - game_objects: - - name: "Ice Pyramid 5F - Glass Door Left Box" - object_id: 0x69 - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - West Ledge Box" - object_id: 0x6A - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Shelf Box" - object_id: 0x6B - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Leapfrog Box" - object_id: 0x6C - type: "Box" - access: [] - - name: "Ice Pyramid 5F - Glass Door Right Box" - object_id: 0x6D - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - North Box" - object_id: 0x6E - type: "Box" - access: [] - links: - - target_room: 64 - entrance: 134 - teleporter: [74, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 53 - access: ["Bomb", "Claw", "Sword"] -- name: Ice Pyramid 5F Stairs to Ice Golem - id: 67 - game_objects: - - name: "Ice Pyramid 5F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid5FStatue"] - access: ["Sword"] - links: - - target_room: 69 - entrance: 137 - teleporter: [76, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 70 - entrance: 136 - teleporter: [75, 0] - access: [] -- name: Ice Pyramid Climbing Wall Room Lower Space - id: 68 - game_objects: [] - links: - - target_room: 53 - entrance: 139 - teleporter: [78, 0] - access: [] - - target_room: 69 - access: ["Claw"] -- name: Ice Pyramid Climbing Wall Room Upper Space - id: 69 - game_objects: [] - links: - - target_room: 67 - entrance: 140 - teleporter: [79, 0] - access: [] - - target_room: 68 - access: ["Claw"] -- name: Ice Pyramid Ice Golem Room - id: 70 - game_objects: - - name: "Ice Pyramid 6F - Ice Golem Chest" - object_id: 0x0E - type: "Chest" - access: ["IceGolem"] - - name: "Ice Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["IceGolem"] - access: [] - links: - - target_room: 67 - entrance: 141 - teleporter: [80, 0] - access: [] - - target_room: 66 - access: [] -- name: Spencer Waterfall - id: 71 - game_objects: [] - links: - - target_room: 72 - entrance: 143 - teleporter: [81, 0] - access: [] - - target_room: 40 - entrance: 145 - teleporter: [82, 0] - access: [] - - target_room: 40 - entrance: 148 - teleporter: [83, 0] - access: [] -- name: Spencer Cave Normal Main - id: 72 - game_objects: - - name: "Spencer's Cave - Box" - object_id: 0x6F - type: "Box" - access: ["Claw"] - - name: "Spencer's Cave - Spencer" - object_id: 8 - type: "NPC" - access: [] - - name: "Spencer's Cave - Locked Chest" - object_id: 13 - type: "NPC" - access: ["VenusKey"] - links: - - target_room: 71 - entrance: 150 - teleporter: [85, 0] - access: [] -- name: Spencer Cave Normal South Ledge - id: 73 - game_objects: - - name: "Collapse Spencer's Cave" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLiberated"] - access: ["MegaGrenade"] - links: - - target_room: 227 - entrance: 151 - teleporter: [7, 6] - access: [] - - target_room: 203 - access: ["MegaGrenade"] -# - target_room: 72 # access to spencer? -# access: ["MegaGrenade"] -- name: Spencer Cave Caved In Main Loop - id: 203 - game_objects: [] - links: - - target_room: 73 - access: [] - - target_room: 207 - entrance: 156 - teleporter: [36, 8] - access: ["MobiusCrest"] - - target_room: 204 - access: ["Claw"] - - target_room: 205 - access: ["Bomb"] -- name: Spencer Cave Caved In Waters - id: 204 - game_objects: - - name: "Bomb Libra Block" - object_id: 0 - type: "Trigger" - on_trigger: ["SpencerCaveLibraBlockBombed"] - access: ["MegaGrenade", "Claw"] - links: - - target_room: 203 - access: ["Claw"] -- name: Spencer Cave Caved In Libra Nook - id: 205 - game_objects: [] - links: - - target_room: 206 - entrance: 153 - teleporter: [33, 8] - access: ["LibraCrest"] -- name: Spencer Cave Caved In Libra Corridor - id: 206 - game_objects: [] - links: - - target_room: 205 - entrance: 154 - teleporter: [34, 8] - access: ["LibraCrest"] - - target_room: 207 - access: ["SpencerCaveLibraBlockBombed"] -- name: Spencer Cave Caved In Mobius Chest - id: 207 - game_objects: - - name: "Spencer's Cave - Mobius Chest" - object_id: 0x0F - type: "Chest" - access: [] - links: - - target_room: 203 - entrance: 155 - teleporter: [35, 8] - access: ["MobiusCrest"] - - target_room: 206 - access: ["Bomb"] -- name: Wintry Temple Outer Room - id: 74 - game_objects: [] - links: - - target_room: 223 - entrance: 157 - teleporter: [15, 6] - access: [] -- name: Wintry Temple Inner Room - id: 75 - game_objects: - - name: "Wintry Temple - West Box" - object_id: 0x70 - type: "Box" - access: [] - - name: "Wintry Temple - North Box" - object_id: 0x71 - type: "Box" - access: [] - links: - - target_room: 92 - entrance: 158 - teleporter: [62, 8] - access: ["GeminiCrest"] -- name: Fireburg Upper Plaza - id: 76 - game_objects: [] - links: - - target_room: 224 - entrance: 159 - teleporter: [9, 6] - access: [] - - target_room: 80 - entrance: 163 - teleporter: [91, 0] - access: [] - - target_room: 77 - entrance: 164 - teleporter: [98, 8] # original value [16, 2] - access: [] - - target_room: 82 - entrance: 165 - teleporter: [96, 8] # original value [17, 2] - access: [] - - target_room: 208 - access: ["Claw"] -- name: Fireburg Lower Plaza - id: 208 - game_objects: - - name: "Fireburg - Hidden Tunnel Box" - object_id: 0x74 - type: "Box" - access: [] - links: - - target_room: 76 - access: ["Claw"] - - target_room: 78 - entrance: 166 - teleporter: [11, 8] - access: ["MultiKey"] -- name: Reuben's House - id: 77 - game_objects: - - name: "Fireburg - Reuben's House Arion" - object_id: 14 - type: "NPC" - access: ["ReubenDadSaved"] - - name: "Reuben Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Reuben1"] - access: [] - - name: "Fireburg - Reuben's House Box" - object_id: 0x75 - type: "Box" - access: [] - links: - - target_room: 76 - entrance: 167 - teleporter: [98, 3] - access: [] -- name: GrenadeMan's House - id: 78 - game_objects: - - name: "Fireburg - Locked House Man" - object_id: 12 - type: "NPC" - access: [] - links: - - target_room: 208 - entrance: 168 - teleporter: [9, 8] - access: ["MultiKey"] - - target_room: 79 - entrance: 169 - teleporter: [93, 0] - access: [] -- name: GrenadeMan's Mobius Room - id: 79 - game_objects: [] - links: - - target_room: 78 - entrance: 170 - teleporter: [94, 0] - access: [] - - target_room: 161 - entrance: 171 - teleporter: [54, 8] - access: ["MobiusCrest"] -- name: Fireburg Vendor House - id: 80 - game_objects: - - name: "Fireburg - Vendor" - object_id: 11 - type: "NPC" - access: [] - links: - - target_room: 76 - entrance: 172 - teleporter: [95, 0] - access: [] - - target_room: 81 - entrance: 173 - teleporter: [96, 0] - access: [] -- name: Fireburg Gemini Room - id: 81 - game_objects: [] - links: - - target_room: 80 - entrance: 174 - teleporter: [97, 0] - access: [] - - target_room: 43 - entrance: 175 - teleporter: [45, 8] - access: ["GeminiCrest"] -- name: Fireburg Hotel Lobby - id: 82 - game_objects: - - name: "Fireburg - Tristam" - object_id: 10 - type: "NPC" - access: ["Tristam", "TristamBoneItemGiven"] - links: - - target_room: 76 - entrance: 177 - teleporter: [99, 3] - access: [] - - target_room: 83 - entrance: 176 - teleporter: [213, 0] - access: [] -- name: Fireburg Hotel Beds - id: 83 - game_objects: [] - links: - - target_room: 82 - entrance: 178 - teleporter: [214, 0] - access: [] -- name: Mine Exterior North West Platforms - id: 84 - game_objects: [] - links: - - target_room: 224 - entrance: 179 - teleporter: [98, 0] - access: [] - - target_room: 88 - entrance: 181 - teleporter: [20, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] - - target_room: 86 - access: ["Claw"] - - target_room: 87 - access: ["Claw"] -- name: Mine Exterior Central Ledge - id: 85 - game_objects: [] - links: - - target_room: 90 - entrance: 183 - teleporter: [22, 2] - access: ["Bomb"] - - target_room: 84 - access: ["Claw"] -- name: Mine Exterior North Ledge - id: 86 - game_objects: [] - links: - - target_room: 89 - entrance: 182 - teleporter: [21, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] -- name: Mine Exterior South East Platforms - id: 87 - game_objects: - - name: "Jinn" - object_id: 0 - type: "Trigger" - on_trigger: ["Jinn"] - access: [] - links: - - target_room: 91 - entrance: 180 - teleporter: [99, 0] - access: ["Jinn"] - - target_room: 86 - access: [] - - target_room: 85 - access: ["Claw"] -- name: Mine Parallel Room - id: 88 - game_objects: - - name: "Mine - Parallel Room West Box" - object_id: 0x77 - type: "Box" - access: ["Claw"] - - name: "Mine - Parallel Room East Box" - object_id: 0x78 - type: "Box" - access: ["Claw"] - links: - - target_room: 84 - entrance: 185 - teleporter: [100, 3] - access: [] -- name: Mine Crescent Room - id: 89 - game_objects: - - name: "Mine - Crescent Room Chest" - object_id: 0x10 - type: "Chest" - access: [] - links: - - target_room: 86 - entrance: 186 - teleporter: [101, 3] - access: [] -- name: Mine Climbing Room - id: 90 - game_objects: - - name: "Mine - Glitchy Collision Cave Box" - object_id: 0x76 - type: "Box" - access: ["Claw"] - links: - - target_room: 85 - entrance: 187 - teleporter: [102, 3] - access: [] -- name: Mine Cliff - id: 91 - game_objects: - - name: "Mine - Cliff Southwest Box" - object_id: 0x79 - type: "Box" - access: [] - - name: "Mine - Cliff Northwest Box" - object_id: 0x7A - type: "Box" - access: [] - - name: "Mine - Cliff Northeast Box" - object_id: 0x7B - type: "Box" - access: [] - - name: "Mine - Cliff Southeast Box" - object_id: 0x7C - type: "Box" - access: [] - - name: "Mine - Reuben" - object_id: 7 - type: "NPC" - access: ["Reuben1"] - - name: "Reuben's dad Saved" - object_id: 0 - type: "Trigger" - on_trigger: ["ReubenDadSaved"] - access: ["MegaGrenade"] - links: - - target_room: 87 - entrance: 188 - teleporter: [100, 0] - access: [] -- name: Sealed Temple - id: 92 - game_objects: - - name: "Sealed Temple - West Box" - object_id: 0x7D - type: "Box" - access: [] - - name: "Sealed Temple - East Box" - object_id: 0x7E - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 190 - teleporter: [16, 6] - access: [] - - target_room: 75 - entrance: 191 - teleporter: [63, 8] - access: ["GeminiCrest"] -- name: Volcano Base - id: 93 - game_objects: - - name: "Volcano - Base Chest" - object_id: 0x11 - type: "Chest" - access: [] - - name: "Volcano - Base West Box" - object_id: 0x7F - type: "Box" - access: [] - - name: "Volcano - Base East Left Box" - object_id: 0x80 - type: "Box" - access: [] - - name: "Volcano - Base East Right Box" - object_id: 0x81 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 192 - teleporter: [103, 0] - access: [] - - target_room: 98 - entrance: 196 - teleporter: [31, 8] - access: [] - - target_room: 96 - entrance: 197 - teleporter: [30, 8] - access: [] -- name: Volcano Top Left - id: 94 - game_objects: - - name: "Volcano - Medusa Chest" - object_id: 0x12 - type: "Chest" - access: ["Medusa"] - - name: "Medusa" - object_id: 0 - type: "Trigger" - on_trigger: ["Medusa"] - access: [] - - name: "Volcano - Behind Medusa Box" - object_id: 0x82 - type: "Box" - access: [] - links: - - target_room: 209 - entrance: 199 - teleporter: [26, 8] - access: [] -- name: Volcano Top Right - id: 95 - game_objects: - - name: "Volcano - Top of the Volcano Left Box" - object_id: 0x83 - type: "Box" - access: [] - - name: "Volcano - Top of the Volcano Right Box" - object_id: 0x84 - type: "Box" - access: [] - links: - - target_room: 99 - entrance: 200 - teleporter: [79, 8] - access: [] -- name: Volcano Right Path - id: 96 - game_objects: - - name: "Volcano - Right Path Box" - object_id: 0x87 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 201 - teleporter: [15, 8] - access: [] -- name: Volcano Left Path - id: 98 - game_objects: - - name: "Volcano - Left Path Box" - object_id: 0x86 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 204 - teleporter: [27, 8] - access: [] - - target_room: 99 - entrance: 202 - teleporter: [25, 2] - access: [] - - target_room: 209 - entrance: 203 - teleporter: [26, 2] - access: [] -- name: Volcano Cross Left-Right - id: 99 - game_objects: [] - links: - - target_room: 95 - entrance: 206 - teleporter: [29, 8] - access: [] - - target_room: 98 - entrance: 205 - teleporter: [103, 3] - access: [] -- name: Volcano Cross Right-Left - id: 209 - game_objects: - - name: "Volcano - Crossover Section Box" - object_id: 0x85 - type: "Box" - access: [] - links: - - target_room: 98 - entrance: 208 - teleporter: [104, 3] - access: [] - - target_room: 94 - entrance: 207 - teleporter: [28, 8] - access: [] -- name: Lava Dome Inner Ring Main Loop - id: 100 - game_objects: - - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" - object_id: 0x88 - type: "Box" - access: [] - - name: "Lava Dome - Exterior South Cliff Box" - object_id: 0x89 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 209 - teleporter: [104, 0] - access: [] - - target_room: 113 - entrance: 211 - teleporter: [105, 0] - access: [] - - target_room: 114 - entrance: 212 - teleporter: [106, 0] - access: [] - - target_room: 116 - entrance: 213 - teleporter: [108, 0] - access: [] - - target_room: 118 - entrance: 214 - teleporter: [111, 0] - access: [] -- name: Lava Dome Inner Ring Center Ledge - id: 101 - game_objects: - - name: "Lava Dome - Exterior Center Dropoff Ledge Box" - object_id: 0x8A - type: "Box" - access: [] - links: - - target_room: 115 - entrance: 215 - teleporter: [107, 0] - access: [] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Plate Ledge - id: 102 - game_objects: - - name: "Lava Dome Plate" - object_id: 0 - type: "Trigger" - on_trigger: ["LavaDomePlate"] - access: [] - links: - - target_room: 119 - entrance: 216 - teleporter: [109, 0] - access: [] -- name: Lava Dome Inner Ring Upper Ledge West - id: 103 - game_objects: [] - links: - - target_room: 111 - entrance: 219 - teleporter: [112, 0] - access: [] - - target_room: 108 - entrance: 220 - teleporter: [113, 0] - access: [] - - target_room: 104 - access: ["Claw"] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Upper Ledge East - id: 104 - game_objects: [] - links: - - target_room: 110 - entrance: 218 - teleporter: [110, 0] - access: [] - - target_room: 103 - access: ["Claw"] -- name: Lava Dome Inner Ring Big Door Ledge - id: 105 - game_objects: [] - links: - - target_room: 107 - entrance: 221 - teleporter: [114, 0] - access: [] - - target_room: 121 - entrance: 222 - teleporter: [29, 2] - access: ["LavaDomePlate"] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - id: 106 - game_objects: - - name: "Lava Dome - Exterior Dead End Caldera Box" - object_id: 0x8B - type: "Box" - access: [] - links: - - target_room: 120 - entrance: 226 - teleporter: [115, 0] - access: [] -- name: Lava Dome Jump Maze II - id: 107 - game_objects: - - name: "Lava Dome - Gold Maze Northwest Box" - object_id: 0x8C - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southwest Box" - object_id: 0xF6 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Northeast Box" - object_id: 0xF7 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze North Box" - object_id: 0xF8 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Center Box" - object_id: 0xF9 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southeast Box" - object_id: 0xFA - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 227 - teleporter: [116, 0] - access: [] - - target_room: 108 - entrance: 228 - teleporter: [119, 0] - access: [] - - target_room: 120 - entrance: 229 - teleporter: [120, 0] - access: [] -- name: Lava Dome Up-Down Corridor - id: 108 - game_objects: [] - links: - - target_room: 107 - entrance: 231 - teleporter: [118, 0] - access: [] - - target_room: 103 - entrance: 230 - teleporter: [117, 0] - access: [] -- name: Lava Dome Jump Maze I - id: 109 - game_objects: - - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" - object_id: 0x8D - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" - object_id: 0x8E - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Center Box" - object_id: 0x8F - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Southwest Box" - object_id: 0x90 - type: "Box" - access: [] - links: - - target_room: 118 - entrance: 232 - teleporter: [121, 0] - access: [] - - target_room: 111 - entrance: 233 - teleporter: [122, 0] - access: [] -- name: Lava Dome Pointless Room - id: 110 - game_objects: [] - links: - - target_room: 104 - entrance: 234 - teleporter: [123, 0] - access: [] -- name: Lava Dome Lower Moon Helm Room - id: 111 - game_objects: - - name: "Lava Dome - U-Bend Room North Box" - object_id: 0x92 - type: "Box" - access: [] - - name: "Lava Dome - U-Bend Room South Box" - object_id: 0x93 - type: "Box" - access: [] - links: - - target_room: 103 - entrance: 235 - teleporter: [124, 0] - access: [] - - target_room: 109 - entrance: 236 - teleporter: [125, 0] - access: [] -- name: Lava Dome Moon Helm Room - id: 112 - game_objects: - - name: "Lava Dome - Beyond River Room Chest" - object_id: 0x13 - type: "Chest" - access: [] - - name: "Lava Dome - Beyond River Room Box" - object_id: 0x91 - type: "Box" - access: [] - links: - - target_room: 117 - entrance: 237 - teleporter: [126, 0] - access: [] -- name: Lava Dome Three Jumps Room - id: 113 - game_objects: - - name: "Lava Dome - Three Jumps Room Box" - object_id: 0x96 - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 238 - teleporter: [127, 0] - access: [] -- name: Lava Dome Life Chest Room Lower Ledge - id: 114 - game_objects: - - name: "Lava Dome - Gold Bar Room Boulder Chest" - object_id: 0x1C - type: "Chest" - access: ["MegaGrenade"] - links: - - target_room: 100 - entrance: 239 - teleporter: [128, 0] - access: [] - - target_room: 115 - access: ["Claw"] -- name: Lava Dome Life Chest Room Upper Ledge - id: 115 - game_objects: - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" - object_id: 0x94 - type: "Box" - access: [] - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" - object_id: 0x95 - type: "Box" - access: [] - links: - - target_room: 101 - entrance: 240 - teleporter: [129, 0] - access: [] - - target_room: 114 - access: ["Claw"] -- name: Lava Dome Big Jump Room Main Area - id: 116 - game_objects: - - name: "Lava Dome - Lava River Room North Box" - object_id: 0x98 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room East Box" - object_id: 0x99 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room South Box" - object_id: 0x9A - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 241 - teleporter: [133, 0] - access: [] - - target_room: 119 - entrance: 243 - teleporter: [132, 0] - access: [] - - target_room: 117 - access: ["MegaGrenade"] -- name: Lava Dome Big Jump Room MegaGrenade Area - id: 117 - game_objects: [] - links: - - target_room: 112 - entrance: 242 - teleporter: [131, 0] - access: [] - - target_room: 116 - access: ["Bomb"] -- name: Lava Dome Split Corridor - id: 118 - game_objects: - - name: "Lava Dome - Split Corridor Box" - object_id: 0x97 - type: "Box" - access: [] - links: - - target_room: 109 - entrance: 244 - teleporter: [130, 0] - access: [] - - target_room: 100 - entrance: 245 - teleporter: [134, 0] - access: [] -- name: Lava Dome Plate Corridor - id: 119 - game_objects: [] - links: - - target_room: 102 - entrance: 246 - teleporter: [135, 0] - access: [] - - target_room: 116 - entrance: 247 - teleporter: [137, 0] - access: [] -- name: Lava Dome Four Boxes Stairs - id: 120 - game_objects: - - name: "Lava Dome - Caldera Stairway West Left Box" - object_id: 0x9B - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway West Right Box" - object_id: 0x9C - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Left Box" - object_id: 0x9D - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Right Box" - object_id: 0x9E - type: "Box" - access: [] - links: - - target_room: 107 - entrance: 248 - teleporter: [136, 0] - access: [] - - target_room: 106 - entrance: 249 - teleporter: [16, 0] - access: [] -- name: Lava Dome Hydra Room - id: 121 - game_objects: - - name: "Lava Dome - Dualhead Hydra Chest" - object_id: 0x14 - type: "Chest" - access: ["DualheadHydra"] - - name: "Dualhead Hydra" - object_id: 0 - type: "Trigger" - on_trigger: ["DualheadHydra"] - access: [] - - name: "Lava Dome - Hydra Room Northwest Box" - object_id: 0x9F - type: "Box" - access: [] - - name: "Lava Dome - Hydra Room Southweast Box" - object_id: 0xA0 - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 250 - teleporter: [105, 3] - access: [] - - target_room: 122 - entrance: 251 - teleporter: [138, 0] - access: ["DualheadHydra"] -- name: Lava Dome Escape Corridor - id: 122 - game_objects: [] - links: - - target_room: 121 - entrance: 253 - teleporter: [139, 0] - access: [] -- name: Rope Bridge - id: 123 - game_objects: - - name: "Rope Bridge - West Box" - object_id: 0xA3 - type: "Box" - access: [] - - name: "Rope Bridge - East Box" - object_id: 0xA4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 255 - teleporter: [140, 0] - access: [] -- name: Alive Forest - id: 124 - game_objects: - - name: "Alive Forest - Tree Stump Chest" - object_id: 0x15 - type: "Chest" - access: ["Axe"] - - name: "Alive Forest - Near Entrance Box" - object_id: 0xA5 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - After Bridge Box" - object_id: 0xA6 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - Gemini Stump Box" - object_id: 0xA7 - type: "Box" - access: ["Axe"] - links: - - target_room: 226 - entrance: 272 - teleporter: [142, 0] - access: ["Axe"] - - target_room: 21 - entrance: 275 - teleporter: [64, 8] - access: ["LibraCrest", "Axe"] - - target_room: 22 - entrance: 276 - teleporter: [65, 8] - access: ["GeminiCrest", "Axe"] - - target_room: 23 - entrance: 277 - teleporter: [66, 8] - access: ["MobiusCrest", "Axe"] - - target_room: 125 - entrance: 274 - teleporter: [143, 0] - access: ["Axe"] -- name: Giant Tree 1F Main Area - id: 125 - game_objects: - - name: "Giant Tree 1F - Northwest Box" - object_id: 0xA8 - type: "Box" - access: [] - - name: "Giant Tree 1F - Southwest Box" - object_id: 0xA9 - type: "Box" - access: [] - - name: "Giant Tree 1F - Center Box" - object_id: 0xAA - type: "Box" - access: [] - - name: "Giant Tree 1F - East Box" - object_id: 0xAB - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 278 - teleporter: [56, 1] # [49, 8] script restored if no map shuffling - access: [] - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 1F North Island - id: 202 - game_objects: [] - links: - - target_room: 127 - entrance: 280 - teleporter: [144, 0] - access: [] - - target_room: 125 - access: ["DragonClaw"] -- name: Giant Tree 1F Central Island - id: 126 - game_objects: [] - links: - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 2F Main Lobby - id: 127 - game_objects: - - name: "Giant Tree 2F - North Box" - object_id: 0xAC - type: "Box" - access: [] - links: - - target_room: 126 - access: ["DragonClaw"] - - target_room: 125 - entrance: 281 - teleporter: [145, 0] - access: [] - - target_room: 133 - entrance: 283 - teleporter: [149, 0] - access: [] - - target_room: 129 - access: ["DragonClaw"] -- name: Giant Tree 2F West Ledge - id: 128 - game_objects: - - name: "Giant Tree 2F - Dropdown Ledge Box" - object_id: 0xAE - type: "Box" - access: [] - links: - - target_room: 140 - entrance: 284 - teleporter: [147, 0] - access: ["Sword"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Lower Area - id: 129 - game_objects: - - name: "Giant Tree 2F - South Box" - object_id: 0xAD - type: "Box" - access: [] - links: - - target_room: 130 - access: ["Claw"] - - target_room: 131 - access: ["Claw"] -- name: Giant Tree 2F Central Island - id: 130 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 135 - entrance: 282 - teleporter: [146, 0] - access: ["Sword"] -- name: Giant Tree 2F East Ledge - id: 131 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Meteor Chest Room - id: 132 - game_objects: - - name: "Giant Tree 2F - Gidrah Chest" - object_id: 0x16 - type: "Chest" - access: [] - links: - - target_room: 133 - entrance: 285 - teleporter: [148, 0] - access: [] -- name: Giant Tree 2F Mushroom Room - id: 133 - game_objects: - - name: "Giant Tree 2F - Mushroom Tunnel West Box" - object_id: 0xAF - type: "Box" - access: ["Axe"] - - name: "Giant Tree 2F - Mushroom Tunnel East Box" - object_id: 0xB0 - type: "Box" - access: ["Axe"] - links: - - target_room: 127 - entrance: 286 - teleporter: [150, 0] - access: ["Axe"] - - target_room: 132 - entrance: 287 - teleporter: [151, 0] - access: ["Axe", "Gidrah"] -- name: Giant Tree 3F Central Island - id: 135 - game_objects: - - name: "Giant Tree 3F - Central Island Box" - object_id: 0xB3 - type: "Box" - access: [] - links: - - target_room: 130 - entrance: 288 - teleporter: [152, 0] - access: [] - - target_room: 136 - access: ["Claw"] - - target_room: 137 - access: ["DragonClaw"] -- name: Giant Tree 3F Central Area - id: 136 - game_objects: - - name: "Giant Tree 3F - Center North Box" - object_id: 0xB1 - type: "Box" - access: [] - - name: "Giant Tree 3F - Center West Box" - object_id: 0xB2 - type: "Box" - access: [] - links: - - target_room: 135 - access: ["Claw"] - - target_room: 127 - access: [] - - target_room: 131 - access: [] -- name: Giant Tree 3F Lower Ledge - id: 137 - game_objects: [] - links: - - target_room: 135 - access: ["DragonClaw"] - - target_room: 142 - entrance: 289 - teleporter: [153, 0] - access: ["Sword"] -- name: Giant Tree 3F West Area - id: 138 - game_objects: - - name: "Giant Tree 3F - West Side Box" - object_id: 0xB4 - type: "Box" - access: [] - links: - - target_room: 128 - access: [] - - target_room: 210 - entrance: 290 - teleporter: [154, 0] - access: [] -- name: Giant Tree 3F Middle Up Island - id: 139 - game_objects: [] - links: - - target_room: 136 - access: ["Claw"] -- name: Giant Tree 3F West Platform - id: 140 - game_objects: [] - links: - - target_room: 139 - access: ["Claw"] - - target_room: 141 - access: ["Claw"] - - target_room: 128 - entrance: 291 - teleporter: [155, 0] - access: [] -- name: Giant Tree 3F North Ledge - id: 141 - game_objects: [] - links: - - target_room: 143 - entrance: 292 - teleporter: [156, 0] - access: ["Sword"] - - target_room: 139 - access: ["Claw"] - - target_room: 136 - access: ["Claw"] -- name: Giant Tree Worm Room Upper Ledge - id: 142 - game_objects: - - name: "Giant Tree 3F - Worm Room North Box" - object_id: 0xB5 - type: "Box" - access: ["Axe"] - - name: "Giant Tree 3F - Worm Room South Box" - object_id: 0xB6 - type: "Box" - access: ["Axe"] - links: - - target_room: 137 - entrance: 293 - teleporter: [157, 0] - access: ["Axe"] - - target_room: 210 - access: ["Axe", "Claw"] -- name: Giant Tree Worm Room Lower Ledge - id: 210 - game_objects: [] - links: - - target_room: 138 - entrance: 294 - teleporter: [158, 0] - access: [] -- name: Giant Tree 4F Lower Floor - id: 143 - game_objects: [] - links: - - target_room: 141 - entrance: 295 - teleporter: [159, 0] - access: [] - - target_room: 148 - entrance: 296 - teleporter: [160, 0] - access: [] - - target_room: 148 - entrance: 297 - teleporter: [161, 0] - access: [] - - target_room: 147 - entrance: 298 - teleporter: [162, 0] - access: ["Sword"] -- name: Giant Tree 4F Middle Floor - id: 144 - game_objects: - - name: "Giant Tree 4F - Highest Platform North Box" - object_id: 0xB7 - type: "Box" - access: [] - - name: "Giant Tree 4F - Highest Platform South Box" - object_id: 0xB8 - type: "Box" - access: [] - links: - - target_room: 149 - entrance: 299 - teleporter: [163, 0] - access: [] - - target_room: 145 - access: ["Claw"] - - target_room: 146 - access: ["DragonClaw"] -- name: Giant Tree 4F Upper Floor - id: 145 - game_objects: [] - links: - - target_room: 150 - entrance: 300 - teleporter: [164, 0] - access: ["Sword"] - - target_room: 144 - access: ["Claw"] -- name: Giant Tree 4F South Ledge - id: 146 - game_objects: - - name: "Giant Tree 4F - Hook Ledge Northeast Box" - object_id: 0xB9 - type: "Box" - access: [] - - name: "Giant Tree 4F - Hook Ledge Southwest Box" - object_id: 0xBA - type: "Box" - access: [] - links: - - target_room: 144 - access: ["DragonClaw"] -- name: Giant Tree 4F Slime Room East Area - id: 147 - game_objects: - - name: "Giant Tree 4F - East Slime Room Box" - object_id: 0xBC - type: "Box" - access: ["Axe"] - links: - - target_room: 143 - entrance: 304 - teleporter: [168, 0] - access: [] -- name: Giant Tree 4F Slime Room West Area - id: 148 - game_objects: [] - links: - - target_room: 143 - entrance: 303 - teleporter: [167, 0] - access: ["Axe"] - - target_room: 143 - entrance: 302 - teleporter: [166, 0] - access: ["Axe"] - - target_room: 149 - access: ["Axe", "Claw"] -- name: Giant Tree 4F Slime Room Platform - id: 149 - game_objects: - - name: "Giant Tree 4F - West Slime Room Box" - object_id: 0xBB - type: "Box" - access: [] - links: - - target_room: 144 - entrance: 301 - teleporter: [165, 0] - access: [] - - target_room: 148 - access: ["Claw"] -- name: Giant Tree 5F Lower Area - id: 150 - game_objects: - - name: "Giant Tree 5F - Northwest Left Box" - object_id: 0xBD - type: "Box" - access: [] - - name: "Giant Tree 5F - Northwest Right Box" - object_id: 0xBE - type: "Box" - access: [] - - name: "Giant Tree 5F - South Left Box" - object_id: 0xBF - type: "Box" - access: [] - - name: "Giant Tree 5F - South Right Box" - object_id: 0xC0 - type: "Box" - access: [] - links: - - target_room: 145 - entrance: 305 - teleporter: [169, 0] - access: [] - - target_room: 151 - access: ["Claw"] - - target_room: 143 - access: [] -- name: Giant Tree 5F Gidrah Platform - id: 151 - game_objects: - - name: "Gidrah" - object_id: 0 - type: "Trigger" - on_trigger: ["Gidrah"] - access: [] - links: - - target_room: 150 - access: ["Claw"] -- name: Kaidge Temple Lower Ledge - id: 152 - game_objects: [] - links: - - target_room: 226 - entrance: 307 - teleporter: [18, 6] - access: [] - - target_room: 153 - access: ["Claw"] -- name: Kaidge Temple Upper Ledge - id: 153 - game_objects: - - name: "Kaidge Temple - Box" - object_id: 0xC1 - type: "Box" - access: [] - links: - - target_room: 185 - entrance: 308 - teleporter: [71, 8] - access: ["MobiusCrest"] - - target_room: 152 - access: ["Claw"] -- name: Windhole Temple - id: 154 - game_objects: - - name: "Windhole Temple - Box" - object_id: 0xC2 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 309 - teleporter: [173, 0] - access: [] -- name: Mount Gale - id: 155 - game_objects: - - name: "Mount Gale - Dullahan Chest" - object_id: 0x17 - type: "Chest" - access: ["DragonClaw", "Dullahan"] - - name: "Dullahan" - object_id: 0 - type: "Trigger" - on_trigger: ["Dullahan"] - access: ["DragonClaw"] - - name: "Mount Gale - East Box" - object_id: 0xC3 - type: "Box" - access: ["DragonClaw"] - - name: "Mount Gale - West Box" - object_id: 0xC4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 310 - teleporter: [174, 0] - access: [] -- name: Windia - id: 156 - game_objects: [] - links: - - target_room: 226 - entrance: 312 - teleporter: [10, 6] - access: [] - - target_room: 157 - entrance: 320 - teleporter: [30, 5] - access: [] - - target_room: 163 - entrance: 321 - teleporter: [97, 8] - access: [] - - target_room: 165 - entrance: 322 - teleporter: [32, 5] - access: [] - - target_room: 159 - entrance: 323 - teleporter: [176, 4] - access: [] - - target_room: 160 - entrance: 324 - teleporter: [177, 4] - access: [] -- name: Otto's House - id: 157 - game_objects: - - name: "Otto" - object_id: 0 - type: "Trigger" - on_trigger: ["RainbowBridge"] - access: ["ThunderRock"] - links: - - target_room: 156 - entrance: 327 - teleporter: [106, 3] - access: [] - - target_room: 158 - entrance: 326 - teleporter: [33, 2] - access: [] -- name: Otto's Attic - id: 158 - game_objects: - - name: "Windia - Otto's Attic Box" - object_id: 0xC5 - type: "Box" - access: [] - links: - - target_room: 157 - entrance: 328 - teleporter: [107, 3] - access: [] -- name: Windia Kid House - id: 159 - game_objects: [] - links: - - target_room: 156 - entrance: 329 - teleporter: [178, 0] - access: [] - - target_room: 161 - entrance: 330 - teleporter: [180, 0] - access: [] -- name: Windia Old People House - id: 160 - game_objects: [] - links: - - target_room: 156 - entrance: 331 - teleporter: [179, 0] - access: [] - - target_room: 162 - entrance: 332 - teleporter: [181, 0] - access: [] -- name: Windia Kid House Basement - id: 161 - game_objects: [] - links: - - target_room: 159 - entrance: 333 - teleporter: [182, 0] - access: [] - - target_room: 79 - entrance: 334 - teleporter: [44, 8] - access: ["MobiusCrest"] -- name: Windia Old People House Basement - id: 162 - game_objects: - - name: "Windia - Mobius Basement West Box" - object_id: 0xC8 - type: "Box" - access: [] - - name: "Windia - Mobius Basement East Box" - object_id: 0xC9 - type: "Box" - access: [] - links: - - target_room: 160 - entrance: 335 - teleporter: [183, 0] - access: [] - - target_room: 186 - entrance: 336 - teleporter: [43, 8] - access: ["MobiusCrest"] -- name: Windia Inn Lobby - id: 163 - game_objects: [] - links: - - target_room: 156 - entrance: 338 - teleporter: [135, 3] - access: [] - - target_room: 164 - entrance: 337 - teleporter: [102, 8] - access: [] -- name: Windia Inn Beds - id: 164 - game_objects: - - name: "Windia - Inn Bedroom North Box" - object_id: 0xC6 - type: "Box" - access: [] - - name: "Windia - Inn Bedroom South Box" - object_id: 0xC7 - type: "Box" - access: [] - - name: "Windia - Kaeli" - object_id: 15 - type: "NPC" - access: ["Kaeli2"] - links: - - target_room: 163 - entrance: 339 - teleporter: [216, 0] - access: [] -- name: Windia Vendor House - id: 165 - game_objects: - - name: "Windia - Vendor" - object_id: 16 - type: "NPC" - access: [] - links: - - target_room: 156 - entrance: 340 - teleporter: [108, 3] - access: [] -- name: Pazuzu Tower 1F Main Lobby - id: 166 - game_objects: - - name: "Pazuzu 1F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu1F"] - access: [] - links: - - target_room: 226 - entrance: 341 - teleporter: [184, 0] - access: [] - - target_room: 180 - entrance: 345 - teleporter: [185, 0] - access: [] -- name: Pazuzu Tower 1F Boxes Room - id: 167 - game_objects: - - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" - object_id: 0xCA - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" - object_id: 0xCB - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" - object_id: 0xCC - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Box" - object_id: 0xCD - type: "Box" - access: [] - links: - - target_room: 169 - entrance: 349 - teleporter: [187, 0] - access: [] -- name: Pazuzu Tower 1F Southern Platform - id: 168 - game_objects: [] - links: - - target_room: 169 - entrance: 346 - teleporter: [186, 0] - access: [] - - target_room: 166 - access: ["DragonClaw"] -- name: Pazuzu 2F - id: 169 - game_objects: - - name: "Pazuzu's Tower 2F - East Room West Box" - object_id: 0xCE - type: "Box" - access: [] - - name: "Pazuzu's Tower 2F - East Room East Box" - object_id: 0xCF - type: "Box" - access: [] - - name: "Pazuzu 2F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2FLock"] - access: ["Axe"] - - name: "Pazuzu 2F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 350 - teleporter: [188, 0] - access: [] - - target_room: 168 - entrance: 351 - teleporter: [189, 0] - access: [] - - target_room: 167 - entrance: 352 - teleporter: [190, 0] - access: [] - - target_room: 171 - entrance: 353 - teleporter: [191, 0] - access: [] -- name: Pazuzu 3F Main Room - id: 170 - game_objects: - - name: "Pazuzu's Tower 3F - Guest Room West Box" - object_id: 0xD0 - type: "Box" - access: [] - - name: "Pazuzu's Tower 3F - Guest Room East Box" - object_id: 0xD1 - type: "Box" - access: [] - - name: "Pazuzu 3F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu3F"] - access: [] - links: - - target_room: 180 - entrance: 356 - teleporter: [192, 0] - access: [] - - target_room: 181 - entrance: 357 - teleporter: [193, 0] - access: [] -- name: Pazuzu 3F Central Island - id: 171 - game_objects: [] - links: - - target_room: 169 - entrance: 360 - teleporter: [194, 0] - access: [] - - target_room: 170 - access: ["DragonClaw"] - - target_room: 172 - access: ["DragonClaw"] -- name: Pazuzu 3F Southern Island - id: 172 - game_objects: - - name: "Pazuzu's Tower 3F - South Ledge Box" - object_id: 0xD2 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 361 - teleporter: [195, 0] - access: [] - - target_room: 171 - access: ["DragonClaw"] -- name: Pazuzu 4F - id: 173 - game_objects: - - name: "Pazuzu's Tower 4F - Elevator West Box" - object_id: 0xD3 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - Elevator East Box" - object_id: 0xD4 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - East Storage Room Chest" - object_id: 0x18 - type: "Chest" - access: [] - - name: "Pazuzu 4F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4FLock"] - access: ["Axe"] - - name: "Pazuzu 4F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 362 - teleporter: [196, 0] - access: [] - - target_room: 184 - entrance: 363 - teleporter: [197, 0] - access: [] - - target_room: 172 - entrance: 364 - teleporter: [198, 0] - access: [] - - target_room: 175 - entrance: 365 - teleporter: [199, 0] - access: [] -- name: Pazuzu 5F Pazuzu Loop - id: 174 - game_objects: - - name: "Pazuzu 5F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu5F"] - access: [] - links: - - target_room: 181 - entrance: 368 - teleporter: [200, 0] - access: [] - - target_room: 182 - entrance: 369 - teleporter: [201, 0] - access: [] -- name: Pazuzu 5F Upper Loop - id: 175 - game_objects: - - name: "Pazuzu's Tower 5F - North Box" - object_id: 0xD5 - type: "Box" - access: [] - - name: "Pazuzu's Tower 5F - South Box" - object_id: 0xD6 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 370 - teleporter: [202, 0] - access: [] - - target_room: 176 - entrance: 371 - teleporter: [203, 0] - access: [] -- name: Pazuzu 6F - id: 176 - game_objects: - - name: "Pazuzu's Tower 6F - Box" - object_id: 0xD7 - type: "Box" - access: [] - - name: "Pazuzu's Tower 6F - Chest" - object_id: 0x19 - type: "Chest" - access: [] - - name: "Pazuzu 6F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6FLock"] - access: ["Bomb", "Axe"] - - name: "Pazuzu 6F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6F"] - access: ["Bomb"] - links: - - target_room: 184 - entrance: 374 - teleporter: [204, 0] - access: [] - - target_room: 175 - entrance: 375 - teleporter: [205, 0] - access: [] - - target_room: 178 - entrance: 376 - teleporter: [206, 0] - access: [] - - target_room: 178 - entrance: 377 - teleporter: [207, 0] - access: [] -- name: Pazuzu 7F Southwest Area - id: 177 - game_objects: [] - links: - - target_room: 182 - entrance: 380 - teleporter: [26, 0] - access: [] - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 7F Rest of the Area - id: 178 - game_objects: [] - links: - - target_room: 177 - access: ["DragonClaw"] - - target_room: 176 - entrance: 381 - teleporter: [27, 0] - access: [] - - target_room: 176 - entrance: 382 - teleporter: [28, 0] - access: [] - - target_room: 179 - access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] -- name: Pazuzu 7F Sky Room - id: 179 - game_objects: - - name: "Pazuzu's Tower 7F - Pazuzu Chest" - object_id: 0x1A - type: "Chest" - access: [] - - name: "Pazuzu" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu"] - access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] - links: - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 1F to 3F - id: 180 - game_objects: [] - links: - - target_room: 166 - entrance: 385 - teleporter: [29, 0] - access: [] - - target_room: 170 - entrance: 386 - teleporter: [30, 0] - access: [] -- name: Pazuzu 3F to 5F - id: 181 - game_objects: [] - links: - - target_room: 170 - entrance: 387 - teleporter: [40, 0] - access: [] - - target_room: 174 - entrance: 388 - teleporter: [41, 0] - access: [] -- name: Pazuzu 5F to 7F - id: 182 - game_objects: [] - links: - - target_room: 174 - entrance: 389 - teleporter: [38, 0] - access: [] - - target_room: 177 - entrance: 390 - teleporter: [39, 0] - access: [] -- name: Pazuzu 2F to 4F - id: 183 - game_objects: [] - links: - - target_room: 169 - entrance: 391 - teleporter: [21, 0] - access: [] - - target_room: 173 - entrance: 392 - teleporter: [22, 0] - access: [] -- name: Pazuzu 4F to 6F - id: 184 - game_objects: [] - links: - - target_room: 173 - entrance: 393 - teleporter: [2, 0] - access: [] - - target_room: 176 - entrance: 394 - teleporter: [3, 0] - access: [] -- name: Light Temple - id: 185 - game_objects: - - name: "Light Temple - Box" - object_id: 0xD8 - type: "Box" - access: [] - links: - - target_room: 230 - entrance: 395 - teleporter: [19, 6] - access: [] - - target_room: 153 - entrance: 396 - teleporter: [70, 8] - access: ["MobiusCrest"] -- name: Ship Dock - id: 186 - game_objects: - - name: "Ship Dock Access" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipDockAccess"] - access: [] - links: - - target_room: 228 - entrance: 399 - teleporter: [17, 6] - access: [] - - target_room: 162 - entrance: 397 - teleporter: [61, 8] - access: ["MobiusCrest"] -- name: Mac Ship Deck - id: 187 - game_objects: - - name: "Mac Ship Steering Wheel" - object_id: 00 - type: "Trigger" - on_trigger: ["ShipSteeringWheel"] - access: [] - - name: "Mac's Ship Deck - North Box" - object_id: 0xD9 - type: "Box" - access: [] - - name: "Mac's Ship Deck - Center Box" - object_id: 0xDA - type: "Box" - access: [] - - name: "Mac's Ship Deck - South Box" - object_id: 0xDB - type: "Box" - access: [] - links: - - target_room: 229 - entrance: 400 - teleporter: [37, 8] - access: [] - - target_room: 188 - entrance: 401 - teleporter: [50, 8] - access: [] - - target_room: 188 - entrance: 402 - teleporter: [51, 8] - access: [] - - target_room: 188 - entrance: 403 - teleporter: [52, 8] - access: [] - - target_room: 189 - entrance: 404 - teleporter: [53, 8] - access: [] -- name: Mac Ship B1 Outer Ring - id: 188 - game_objects: - - name: "Mac's Ship B1 - Northwest Hook Platform Box" - object_id: 0xE4 - type: "Box" - access: ["DragonClaw"] - - name: "Mac's Ship B1 - Center Hook Platform Box" - object_id: 0xE5 - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 187 - entrance: 405 - teleporter: [208, 0] - access: [] - - target_room: 187 - entrance: 406 - teleporter: [175, 0] - access: [] - - target_room: 187 - entrance: 407 - teleporter: [172, 0] - access: [] - - target_room: 193 - entrance: 408 - teleporter: [88, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B1 Square Room - id: 189 - game_objects: [] - links: - - target_room: 187 - entrance: 409 - teleporter: [141, 0] - access: [] - - target_room: 192 - entrance: 410 - teleporter: [87, 0] - access: [] -- name: Mac Ship B1 Central Corridor - id: 190 - game_objects: - - name: "Mac's Ship B1 - Central Corridor Box" - object_id: 0xE6 - type: "Box" - access: [] - links: - - target_room: 192 - entrance: 413 - teleporter: [86, 0] - access: [] - - target_room: 191 - entrance: 412 - teleporter: [102, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B2 South Corridor - id: 191 - game_objects: [] - links: - - target_room: 190 - entrance: 415 - teleporter: [55, 8] - access: [] - - target_room: 194 - entrance: 414 - teleporter: [57, 1] - access: [] -- name: Mac Ship B2 North Corridor - id: 192 - game_objects: [] - links: - - target_room: 190 - entrance: 416 - teleporter: [56, 8] - access: [] - - target_room: 189 - entrance: 417 - teleporter: [57, 8] - access: [] -- name: Mac Ship B2 Outer Ring - id: 193 - game_objects: - - name: "Mac's Ship B2 - Barrel Room South Box" - object_id: 0xDF - type: "Box" - access: [] - - name: "Mac's Ship B2 - Barrel Room North Box" - object_id: 0xE0 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southwest Room Box" - object_id: 0xE1 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southeast Room Box" - object_id: 0xE2 - type: "Box" - access: [] - links: - - target_room: 188 - entrance: 418 - teleporter: [58, 8] - access: [] -- name: Mac Ship B1 Mac Room - id: 194 - game_objects: - - name: "Mac's Ship B1 - Mac Room Chest" - object_id: 0x1B - type: "Chest" - access: [] - - name: "Captain Mac" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLoaned"] - access: ["CaptainCap"] - links: - - target_room: 191 - entrance: 424 - teleporter: [101, 0] - access: [] -- name: Doom Castle Corridor of Destiny - id: 195 - game_objects: [] - links: - - target_room: 201 - entrance: 428 - teleporter: [84, 0] - access: [] - - target_room: 196 - entrance: 429 - teleporter: [35, 2] - access: [] - - target_room: 197 - entrance: 430 - teleporter: [209, 0] - access: ["StoneGolem"] - - target_room: 198 - entrance: 431 - teleporter: [211, 0] - access: ["StoneGolem", "TwinheadWyvern"] - - target_room: 199 - entrance: 432 - teleporter: [13, 2] - access: ["StoneGolem", "TwinheadWyvern", "Zuh"] -- name: Doom Castle Ice Floor - id: 196 - game_objects: - - name: "Doom Castle 4F - Northwest Room Box" - object_id: 0xE7 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Southwest Room Box" - object_id: 0xE8 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Northeast Room Box" - object_id: 0xE9 - type: "Box" - access: ["Sword"] - - name: "Doom Castle 4F - Southeast Room Box" - object_id: 0xEA - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Stone Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["StoneGolem"] - access: ["Sword", "DragonClaw"] - links: - - target_room: 195 - entrance: 433 - teleporter: [109, 3] - access: [] -- name: Doom Castle Lava Floor - id: 197 - game_objects: - - name: "Doom Castle 5F - North Left Box" - object_id: 0xEB - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - North Right Box" - object_id: 0xEC - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Left Box" - object_id: 0xED - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Right Box" - object_id: 0xEE - type: "Box" - access: ["DragonClaw"] - - name: "Twinhead Wyvern" - object_id: 0 - type: "Trigger" - on_trigger: ["TwinheadWyvern"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 434 - teleporter: [210, 0] - access: [] -- name: Doom Castle Sky Floor - id: 198 - game_objects: - - name: "Doom Castle 6F - West Box" - object_id: 0xEF - type: "Box" - access: [] - - name: "Doom Castle 6F - East Box" - object_id: 0xF0 - type: "Box" - access: [] - - name: "Zuh" - object_id: 0 - type: "Trigger" - on_trigger: ["Zuh"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 435 - teleporter: [212, 0] - access: [] - - target_room: 197 - access: [] -- name: Doom Castle Hero Room - id: 199 - game_objects: - - name: "Doom Castle Hero Chest 01" - object_id: 0xF2 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 02" - object_id: 0xF3 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 03" - object_id: 0xF4 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 04" - object_id: 0xF5 - type: "Chest" - access: [] - links: - - target_room: 200 - entrance: 436 - teleporter: [54, 0] - access: [] - - target_room: 195 - entrance: 441 - teleporter: [110, 3] - access: [] -- name: Doom Castle Dark King Room - id: 200 - game_objects: [] - links: - - target_room: 199 - entrance: 442 - teleporter: [52, 0] - access: [] From 29a0b013cb3ecfb186939c558bbc608ff00c8447 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:47:19 -0400 Subject: [PATCH 070/393] KH2: Hotfix update for game verison 1.0.0.9 (#3534) * update the addresses hopefully * todo * update address for steam and epic * oops * leftover hard address * made auto tracking say which version of the game * not needed anymore since they were updated --- worlds/kh2/Client.py | 67 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 513d85257b97..e2d2338b7651 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -116,12 +116,19 @@ def __init__(self, server_address, password): # self.inBattle = 0x2A0EAC4 + 0x40 # self.onDeath = 0xAB9078 # PC Address anchors - self.Now = 0x0714DB8 - self.Save = 0x09A70B0 + # self.Now = 0x0714DB8 old address + # epic addresses + self.Now = 0x0716DF8 + self.Save = 0x09A92F0 + self.Journal = 0x743260 + self.Shop = 0x743350 + self.Slot1 = 0x2A22FD8 # self.Sys3 = 0x2A59DF0 # self.Bt10 = 0x2A74880 # self.BtlEnd = 0x2A0D3E0 - self.Slot1 = 0x2A20C98 + # self.Slot1 = 0x2A20C98 old address + + self.kh2_game_version = None # can be egs or steam self.chest_set = set(exclusion_table["Chests"]) self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) @@ -228,6 +235,9 @@ def kh2_read_int(self, address): def kh2_write_int(self, address, value): self.kh2.write_int(self.kh2.base_address + address, value) + def kh2_read_string(self, address, length): + return self.kh2.read_string(self.kh2.base_address + address, length) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -367,10 +377,26 @@ def on_package(self, cmd: str, args: dict): for weapon_location in all_weapon_slot: all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location]) self.all_weapon_location_id = set(all_weapon_location_id) + try: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - logger.info("You are now auto-tracking") - self.kh2connected = True + if self.kh2_game_version is None: + if self.kh2_read_string(0x09A9830, 4) == "KH2J": + self.kh2_game_version = "STEAM" + self.Now = 0x0717008 + self.Save = 0x09A9830 + self.Slot1 = 0x2A23518 + self.Journal = 0x7434E0 + self.Shop = 0x7435D0 + + elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": + self.kh2_game_version = "EGS" + else: + self.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if self.kh2_game_version is not None: + logger.info(f"You are now auto-tracking. {self.kh2_game_version}") + self.kh2connected = True except Exception as e: if self.kh2connected: @@ -589,8 +615,8 @@ async def IsInShop(self, sellable): # if journal=-1 and shop = 5 then in shop # if journal !=-1 and shop = 10 then journal - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): # print("your in the shop") sellable_dict = {} @@ -599,8 +625,8 @@ async def IsInShop(self, sellable): amount = self.kh2_read_byte(self.Save + itemdata.memaddr) sellable_dict[itemName] = amount while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) await asyncio.sleep(0.5) for item, amount in sellable_dict.items(): itemdata = self.item_name_to_data[item] @@ -750,7 +776,7 @@ async def verifyItems(self): item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}: + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}: self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: @@ -802,7 +828,7 @@ async def verifyItems(self): self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) elif self.base_item_slots + amount_of_items < 8: self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) - + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: @@ -905,8 +931,23 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.sleep(15) ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") if ctx.kh2 is not None: - logger.info("You are now auto-tracking") - ctx.kh2connected = True + if ctx.kh2_game_version is None: + if ctx.kh2_read_string(0x09A9830, 4) == "KH2J": + ctx.kh2_game_version = "STEAM" + ctx.Now = 0x0717008 + ctx.Save = 0x09A9830 + ctx.Slot1 = 0x2A23518 + ctx.Journal = 0x7434E0 + ctx.Shop = 0x7435D0 + + elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J": + ctx.kh2_game_version = "EGS" + else: + ctx.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if ctx.kh2_game_version is not None: + logger.info(f"You are now auto-tracking {ctx.kh2_game_version}") + ctx.kh2connected = True except Exception as e: if ctx.kh2connected: ctx.kh2connected = False From ff680b26cc20d7f3392c1f683f5217702e56e326 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 24 Jul 2024 07:49:28 -0400 Subject: [PATCH 071/393] DLC Quest: Add options presets to DLC Quest (#3676) * - Add options presets to DLC Quest * - Removed unused import --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/dlcquest/__init__.py | 2 ++ worlds/dlcquest/presets.py | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 worlds/dlcquest/presets.py diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 2fc0da075d22..b8f2aad6ff94 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,12 +8,14 @@ from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .presets import dlcq_options_presets from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + options_presets = dlcq_options_presets option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", diff --git a/worlds/dlcquest/presets.py b/worlds/dlcquest/presets.py new file mode 100644 index 000000000000..ccfd79399521 --- /dev/null +++ b/worlds/dlcquest/presets.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle + +all_random_settings = { + DoubleJumpGlitch.internal_name: "random", + CoinSanity.internal_name: "random", + CoinSanityRange.internal_name: "random", + PermanentCoins.internal_name: "random", + TimeIsMoney.internal_name: "random", + EndingChoice.internal_name: "random", + Campaign.internal_name: "random", + ItemShuffle.internal_name: "random", + "death_link": "random", +} + +main_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_basic, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +lfod_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_live_freemium_or_die, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +easy_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_none, + CoinSanityRange.internal_name: 40, + PermanentCoins.internal_name: PermanentCoins.option_true, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +hard_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_optional, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + + +dlcq_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Main campaign": main_campaign_settings, + "LFOD campaign": lfod_campaign_settings, + "Both easy": easy_settings, + "Both hard": hard_settings, +} From 8756f48e46f0da7685453890b4eccac7f84372d2 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:00:16 -0400 Subject: [PATCH 072/393] [TLOZ]: Fix determinism / Add Location Name Groups / Remove Level 9 Junk Fill (#3670) * [TLOZ]: Fix determinism / Add Location Name Groups / Remove Level 9 Junk Fill Axing the final uses of world.multiworld.random that were missed before, hopefully fixing the determinism issue brought up in Issue #3664 (at least on TLOZ's end, leaving SMZ3 alone). Also adding location name groups finally, as well as axing the Level 9 Junk Fill because with the new location name groups players can choose to exclude Level 9 with exclude locations instead. * location name groups * add take any item and sword cave location name groups * use sets like you're supposed to, silly --- worlds/tloz/ItemPool.py | 10 +--------- worlds/tloz/Locations.py | 8 ++++++++ worlds/tloz/__init__.py | 20 ++++++++++++++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 5b90e99722df..4acda4ef41fc 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -80,7 +80,7 @@ def generate_itempool(tlozworld): location.item.classification = ItemClassification.progression def get_pool_core(world): - random = world.multiworld.random + random = world.random pool = [] placed_items = {} @@ -132,14 +132,6 @@ def get_pool_core(world): else: pool.append(fragment) - # Level 9 junk fill - if world.options.ExpandedPool > 0: - spots = random.sample(level_locations[8], len(level_locations[8]) // 2) - for spot in spots: - junk = random.choice(list(minor_items.keys())) - placed_items[spot] = junk - minor_items[junk] -= 1 - # Finish Pool final_pool = basic_pool if world.options.ExpandedPool: diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 5b30357c940c..9715cc684291 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -99,6 +99,14 @@ "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" ] +take_any_locations = [ + "Take Any Item Left", "Take Any Item Middle", "Take Any Item Right" +] + +sword_cave_locations = [ + "Starting Sword Cave", "White Sword Pond", "Magical Sword Grave" +] + food_locations = [ "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index a1f9081418e4..8ea5f3e18ca1 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -12,7 +12,8 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations from .Items import item_table, item_prices, item_game_ids from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ - standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations, \ + take_any_locations, sword_cave_locations from .Options import TlozOptions from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late from .Rules import set_rules @@ -87,6 +88,21 @@ class TLoZWorld(World): } } + location_name_groups = { + "Shops": set(shop_locations), + "Take Any": set(take_any_locations), + "Sword Caves": set(sword_cave_locations), + "Level 1": set(level_locations[0]), + "Level 2": set(level_locations[1]), + "Level 3": set(level_locations[2]), + "Level 4": set(level_locations[3]), + "Level 5": set(level_locations[4]), + "Level 6": set(level_locations[5]), + "Level 7": set(level_locations[6]), + "Level 8": set(level_locations[7]), + "Level 9": set(level_locations[8]) + } + for k, v in item_name_to_id.items(): item_name_to_id[k] = v + base_id @@ -307,7 +323,7 @@ def modify_multidata(self, multidata: dict): def get_filler_item_name(self) -> str: if self.filler_items is None: self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] - return self.multiworld.random.choice(self.filler_items) + return self.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: if self.options.ExpandedPool: From 1852287c913cf751b219b1f3552aef57744ccc61 Mon Sep 17 00:00:00 2001 From: Ladybunne Date: Wed, 24 Jul 2024 22:07:07 +1000 Subject: [PATCH 073/393] LADX: Add an item group for instruments (#3666) * Add an item group for LADX instruments * Update worlds/ladx/__init__.py Co-authored-by: Scipio Wright * Fix indent depth --------- Co-authored-by: Scipio Wright Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/ladx/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 21876ed671e2..c958ef212fe4 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -98,9 +98,12 @@ class LinksAwakeningWorld(World): # Items can be grouped using their names to allow easy checking if any item # from that group has been collected. Group names can also be used for !hint - #item_name_groups = { - # "weapons": {"sword", "lance"} - #} + item_name_groups = { + "Instruments": { + "Full Moon Cello", "Conch Horn", "Sea Lily's Bell", "Surf Harp", + "Wind Marimba", "Coral Triangle", "Organ of Evening Calm", "Thunder Drum" + }, + } prefill_dungeon_items = None From 878d5141ce2c96aeb9565c4b8eadeedfe2b62242 Mon Sep 17 00:00:00 2001 From: JKLeckr <11635283+JKLeckr@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:08:16 -0400 Subject: [PATCH 074/393] Project: Add .code-workspace wildcard to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5686f43de380..791f7b1bb7fe 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,7 @@ venv/ ENV/ env.bak/ venv.bak/ -.code-workspace +*.code-workspace shell.nix # Spyder project settings From e714d2e129bdb940f923ab6469f289ae9a4a7e58 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 24 Jul 2024 08:34:51 -0400 Subject: [PATCH 075/393] Lingo: Add option to prevent shuffling postgame (#3350) * Lingo: Add option to prevent shuffling postgame * Allow roof access on door shuffle * Fix broken unit test * Simplified THE END edge case * Revert unnecessary change * Review comments * Fix mastery unit test * Update generated.dat * Added player's name to error message --- worlds/lingo/__init__.py | 33 +++++++++++++++- worlds/lingo/data/LL1.yaml | 16 +++++++- worlds/lingo/data/generated.dat | Bin 136277 -> 136563 bytes worlds/lingo/data/ids.yaml | 3 +- worlds/lingo/options.py | 6 +++ worlds/lingo/player_logic.py | 34 +++++++++------- worlds/lingo/rules.py | 3 ++ worlds/lingo/test/TestMastery.py | 6 ++- worlds/lingo/test/TestPostgame.py | 62 ++++++++++++++++++++++++++++++ 9 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 worlds/lingo/test/TestPostgame.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 8d6a7fc4ebee..3b67617873c7 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -3,7 +3,7 @@ """ from logging import warning -from BaseClasses import Item, ItemClassification, Tutorial +from BaseClasses import CollectionState, Item, ItemClassification, Tutorial from Options import OptionError from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance @@ -68,6 +68,37 @@ def generate_early(self): def create_regions(self): create_regions(self) + if not self.options.shuffle_postgame: + state = CollectionState(self.multiworld) + state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True) + + # Note: relies on the assumption that real_items is a definitive list of real progression items in this + # world, and is not modified after being created. + for item in self.player_logic.real_items: + state.collect(self.create_item(item), True) + + # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway. + if self.player_logic.forced_good_item != "": + state.collect(self.create_item(self.player_logic.forced_good_item), True) + + all_locations = self.multiworld.get_locations(self.player) + state.sweep_for_events(locations=all_locations) + + unreachable_locations = [location for location in all_locations + if not state.can_reach_location(location.name, self.player)] + + for location in unreachable_locations: + if location.name in self.player_logic.event_loc_to_item.keys(): + continue + + self.player_logic.real_locations.remove(location.name) + location.parent_region.locations.remove(location) + + if len(self.player_logic.real_items) > len(self.player_logic.real_locations): + raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number" + f" of required items without shuffling the postgame. Either enable postgame" + f" shuffling, or choose different options.") + def create_items(self): pool = [self.create_item(name) for name in self.player_logic.real_items] diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index e12ca022973b..3035446ef793 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -879,6 +879,8 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + - room: Ending Area + panel: THE END paintings: - id: eye_painting disable: True @@ -2322,7 +2324,7 @@ orientation: east - id: hi_solved_painting orientation: west - Orange Tower Seventh Floor: + Ending Area: entrances: Orange Tower Sixth Floor: room: Orange Tower @@ -2334,6 +2336,18 @@ check: True tag: forbid non_counting: True + location_name: Orange Tower Seventh Floor - THE END + doors: + End: + event: True + panels: + - THE END + Orange Tower Seventh Floor: + entrances: + Ending Area: + room: Ending Area + door: End + panels: THE MASTER: # We will set up special rules for this in code. id: Countdown Panels/Panel_master_master diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 3ed6cb24f7d289c38189932fe5ccf705bdaf0262..4a751b25ec5f143b6b055fd2043a6543754ef5b1 100644 GIT binary patch delta 27407 zcma)l34GMW^*EDbcazP1k&9##LK5zTB%Fe7lFdPq-LSinLlFW5!-@$IqJkpc0;K-HneTo#VE@1NQ!?K-Z{EC_dGp?zH*dCY z-D~^L{kEtpqBciviW=-*Ft20W{IT=qbyRkYAJbkrZ^5{-g;fj3mz9qnH>P|{<(R&p zZP9|BQ7hUnT-+CQbj)jC*j`atUQyXzHg;ZRWyONA^T$&lT z+Fe}>7ImHPa4zd;@0-mFT|;n)dRH?tzX{@S(H;`jjrN&{ng-gQnkoX@>3hgI<1d-81( zepWo+yCaS7zb*?Zk6c$u|EBST>ocg7&iAZM;mz0QLZe03=PP15BzV5vy*5w*J-e6pUCGN2}U$D_FdO^WnVykMmBq<(|5<(wQFYg zt#z*nRPZa-4N)y`xgbPZt1MYcNZp{DChC7D96#JZp%ao4#POe_8uQ9g8~_I9y%J zm$i4z@90x=^&C-&mo#yZXiqMe9wz{l+0f%VFeA5!d#4`*5E7f-z$pzTU2j z+m|iTG(H3j0Q0i-`Gek5>j8Kb{qg?FhpivN-s7$7(^U`u;EUG}F}JDe-{vIW9gZ=! zKgM_bE25gQp+L#ofCPT#hAA+p%QsX=_9Kz~U`%WKqORqzA$s~~BvGzQ7A@CNRnLN$-I={`O5ZDTck7A1Q80qM&3Yy-;b&|f4Dh*| z%i!Nln{nqJelDLM-JAlYlbf-Fxtr5?>zG(RXjdA1&Dv7bMBA&M60-UmTn?AJmbmX& zBllZq3-$E|o{JbTU{zl*Y*laL_im}kkfXOXdwhOJY57n+0VlT%0%j9Mrdo#k_$V=1 zPq(whWlZ-ffom_sP=&akza*NYi7GGHo6d%dIRz{Sn4fy{6hK^iv(_Ci^IbQ40RHpM zp!&ofiBxe*p~INa-atmAWZS%G@A2MSjD>f*oKkTFhVxf%DGjyl2@F0>+b?*^t&PTl zJH38m!Mko9W^P=0n#SMq1Gl>L6g0b>b$YH6ZyO3Dn{ZoEoYb+FMA-AgIEzf@m*0l_ z;ECI`zRI~hLCsSNFT1@7>Mpx|Y_T!oV2bnnWgQ*u^A>lUqKyXecW*}vlD#z>dPB|a zcv0($Qr^3@0qUOGikd3<&NOb>maICek8JZ!Qo`@{c>N7( zJfzJn(;MA>rIeLMx79W}TRiU2CUM?@OMlKC#i}PiUvmeliP!It+rF3&-R@9z?Ywon zD^~UvSv^xL=V!OqDugZ`w_~hbB1F}Crn{B;4j<@BzGnySlaF@DfoiF{mOJl6)xO|P zJ=4$MsV86)|KLvNS(37?p|RFwl5-lX0*=wbZw6Lq;7x~e`6YLy!GLeP%b^x}Coc3* zapVP-&ELFhLZ#x~*Xq>UgGfdBUCtKsfhz7KAw_& zjQWw~@E3kJVYTYR?Qu)ZsTL>F*QO~_pV4(~Zud-cr_n8q^$mVAB+BQk3nK!6mT*9g z1}G~YP&034pdD^_D+2uJqM`nkF+u~c^zGzvG){Fn(^LC<+ zyLso(!3I)q=OWNkv93kucPH?HoH$jy|T*QR?fssH00S4llna6DUo+2L+TDqU$$uas0OFLF3iFOK-TV`KDdx0)^wdoB%J|ovXxkeG+Vm z;o>hT5xIQ%?rNbrdVI&9viYaGQ5og$8KjzR!)C>NUvws)y9YhZ<$KWFgP*Jr z?n8u83L%Pry$9F(fBVZ_&uHK93I5XF`eZ3PkzSXylVCBBlM&AihmJb-4Oa5}I^VEw zHqidpKHP`#4{3At4ljS`{A@`h&eu2#w21FQF!Y89KJ-wp*07=I4ffo4B zaSX|(bBALjX!!+w!SryLw&rjGk^P0)cW{x{&hA@d^6&e4I`3Q6R~obywzT&w?^tGd z9w)Iws|LD}9?yI@zrkLStb2Ju@TNlaIx zIt3lQp-(01mE%%*_M?da81^WhMtqMJA_aF|Cf`8dJ02Yk>vQN)y>65F_m2((#A%N? z;-l*ty*@+Uragum4dDEa$Kt3|zz;q)3|gb1jN6~2R=$l7-j5vA@1GQB%usLOcD{bU z_WIlT1N+_MCHrBWkj}OETs2@H%_=t9-_YpQ5MF*@c!+XOU?<=A2Tl1Re(iw?(D~B` zhM7?wHlbX_Qyxc@=EuWO{%As3&+mA=N*mE;kF$=KJ(+K&_m&CyR$lV^N&3W8;{_Yl zANz9?_D=rT@5e#!$A4dHCitxhc5nSi(WsjB!8Ugwjm_?e~I8tDgq_$fT+q(42<)L-YTyn9!UI#*dz_|m7(2Ar3l z&NUq_Qebx#@vlat^So!!udjFpy_S%^%HbQHL6_~BXHZN2 z{7jyb&|+Tr>{K!z-R|Eti|b;_*7&^|~wACBc}Q?%rj~SAfds3r5#_ zUB*cQ9jwF8XM|jV(|G~yICX_`>i(FD8sX6Jbl&jC$w3;jZcp{EQceLfyg_QBk3)x&K9dsNKS!e^KuCfl;W)sp%On>FOQ4?Ijlu zZuG(o{?JS5<})VvP|MD5U)ixN%sYCJfBDj)Tsh|n-bNo-r-trji&l1rT%@j-vxiB9 z$XUjulirH-d8EGzPM$U`{Gpe1sOA}d^kqDFI9|y-WstA&6|bOq*zroW8svAcXuta% zp7d%t)K$MaLe?cj`7iD^Y|N*8)2l9JP$RWBt7RF3MPI#|qm2K*aMkm9`F$CDNh6Uk<;a@@Ps?A^KKqYy^C7LrUdsjc=Dwb;-AaE0$fKjwI0*1xU(3rg zv_det4n)`B-<}Fg+@nAlRVvHzQ7dTi)~&L4PPJ1IH*_1A}tlw*rRM>=Fj zg0_apEC@S_Lue1=k)`-Wc3DZJqg zjQ?HsMrD?1WZ-Fb7$a-oue>otYl^zYv-S49>CkXC{H8;Lx#tiXb?}~&Y*=kt1yUxr zAHoHHE9 zxFZhw2J$0^(@0AMt?u3n=XJo6P{_(chX!L5S99Bu5`EV9xz09Z9(c^*vya%3+?Aaf z{E8#Vp<+y|vHH3;CdN1f^5j1i#e{++$$zS%t=W%}X!oM7&}{DHoqsCUe7H4d#}B@%kAG+KWA7qwBi4H}VTVBI^Zv zRqUP5?Bh>6L-^ekalRc#?BL}`)A%)?1}FAD5eSe(b5eJjtK=*bOtZy^D?F zlRt?Kh3|R;@5yyMO{-VU&wpH|l>AHwfxB|Uyx5-Syn=8lES!$uL24x=dGd*`O}_P;hEw;LB` zfOctaYmwx@dLH@fsL-PI1RjvIPAeuU>AduBuxMMBu#(ZI<>bTP;v|p1j4j3CE>=9| zv`v5f9~}?lfzMp92zx&pVb)Tqv2yB88TVMuKCi>!9+{ub=YO6S^5}X3!(}6j7b05i z#uY;~zvXj0z#RG9G2GBKJ%O==TpEmlUDFZf0=D4P4&kdV$l_IhuNZ4M5?*P(Pcy`z z&E0Wv7>((C+ut!JfBf&6=D3}|o8lDeTCANrzPMZ)G_Tp9?fYW#NQ2kjK&>2DoUh}2 zbiKoRT7uk`^DDnh6VH6i68PjVm+K&+w+*i%3^UC4eKC}O^<{=RJb$629Cn?tniv1$ z%se@4s~?G|Ko5-ExPZL+A3FTKjz92^Y3J)%3YksvBcrREoweHSkv?)LY@{&2Z0Li% z#AvJuYZ2x3c#IfC^06F#`9G6jYO!?xKQY9GrT6}ciVaHf(bJuNBi;@W3^OJl%eB`! zXBw9j*ka`|O=ah?A+X0^KQ>rd;TLd9i+OTF8c+P!MD5eMW`cj_?_OfWspkGGQw5Q2 zFvywBjZI-}hw(uJivnL%vmgN`o(K@*Mk(+&h6j(M32O3UGyI)sCzmhcxhIn1qFOyZ zedWLtPYmK^$I;`!(#+%N1VRaC`fs{J5xQPqiKdRMo(=_)Zk4 zX|ZI`v^bAv@GU3Ct1D-7=WJ|r;&rt;8^cyUs@hqtgIr}*pU*X*7c5zX4jiI^IP@Z!# z9I2r(G$aFQQ5X{6coK!^v6DKSU(Y{2IZeq&w67JmcW43K!Tt=YscWmz3n>96?i7a8 z*JP#>WnxWY_{}>o3DOBLa^}{Z^Q8ae@(=!v!-GwmQ`bTcpbdN|az ze7yjsGx^R`{=(NfuDFwb_%$B(k9?lRM}LEM8SCeKqw62$7kx8X>+9(uF_aV2Ll!O*b841Ll>8u4(qvgsn|PXusY2Z5Z)a zPec48{>`_O)y0=whck-H=Jr47#7js5QTius~LUI%e1g@{`}Ahv@hL?=?I>Xl;=? z5VS=$-}plUx!k*;-TQ%}y=`tI3xSYgeUQlk5v6_; z5>0|+mKaOf*fEt=69^P8G$SY*sVC4(43`EIFn_|ayxZZ2*kOl})^nF!i)j(8XcP)N zgsvo;zM2MCxGlHVIo*DzPtTNaGM1XAv0ZR^pgkQ?X2EkK{>c_gEDXXLvV54)=@Q^M z;s*ve6~Yn0bXe*|(Hz0fFVz(PPyGYs6A>&Yq(gcG?Q(^}r1X6;ERxkvkfftqO(zXG z*lJH(Kc!{y)p(uLwKpK{k7Of(+jk?GHj*a*B z){{iAsFpUrp#gaJ>deGoG+fz)BnDyPgD94SI~U8*(JV!m%cEIInMpR+^coY)=BDm-eo!Q?a3CPxUyH!#R0&a|;&^KIzq_sO|olV=z4j9JA28yh)M&EWrs zA>ta2B%ZeFX1(GfKZap))Dq67ER zY!_R;C)wtXYm7wD-;4X>ST$H2oyJ-$M#i&THINrYYdo8#I*MzUS?l%GyWGZLw3hSC zmw%8^g17EZ`ow zjGOBCnV3f#-3kgR?0gZ~?__BO|Jv6W?q^u)+WLc{S^!NC@2La^Q9AKq0@IGu58{^u z)}l;ROm(BD-s@})cTy}h&KiH4ra>c%h2xmtRqchtsv=EbIP8YoZ<{~-DuuA3>%47^ zVM@?)PK&bzj$UE3`YY7XX(!ywk(CQ3wsx-QSv0@L(a+P!%@V&Ruyipyi6x1tNvuQ- z)e7T0+tsX188K$9_PWk)^oPOF;Rxro#o1r4+u*!bSaqb0WyZp%{>Cn?aM4Lb)rZUYutfMh20=$UvnV$7fNAjO zNjObWlN(|?l7mcfdpgYuAw7|<$B-e8rn5w2JeUrzST46ap?QPL>+nOeOkhS9JP?@O zC;tmKuxr;y!L~NTrB$vRW1!6O00haw80x7alhaFfo&zX@k%S=pT)}VIUYKIeFsAfh@5x~WP=;yEw^DLCG^Q?-a^)JI z%c_Jsm%2oFOtefJS7;1 z5;Qqz$*6p}gPK!$k1W$t0#0nq^v*@gJB*#19Pjf0dydj@u=wB_mXKoF6zW(KmQ5}$ zEnrz-|INwc;x&Sa&k9&Rc`?D*nq}QRJFo2*B& zyO1UVxX%}G6^bhD$VgUT18>+#+m~ua4<_M(a*1r1FV+kO)kksz+Yy4t5K@83rM3`Y z0n-RyfL|bSUx_;fmSkF{|Pw-O}3TZ3TZZq#rsn-D2-t@E1Rx&Fms! z2(v0Zc&La?g=X*vHZ*29V|VRCh&|Xm4gqPWi3#CTins6L=|v0!kWh?|o(YjhZSSbk z8es9_kusJV7s~twy3>kT6?AreF`H;rB>ASQ9PwB1s^t_65+h2$jOfZ=BQnMO653ZV z1&&yVp!z-Fs>&&Gh(jf`op5@rkcCFO2Xq{65X2YmQkpWFOlLEuBgmEo;tpAZSaG;U zsP3sb(E_L6t5L1~Hi!_b9v?Tb)u4w1pv< zKzaf8SBB^wL3{q%5wz$aj2Ytj5qe++R?0w^To;JEku;%bN8%b=Akfl?M^iN>Yb0c$ ztB-tf^GHp@0oD_oSHOaWCkP&w^I#1>DKGn?9L&~vevTfywMD9VY0_8Jr?o-C&k zXnHCf7r-jz?*OY{n=4~2umf8lJP+S6 zz)X?o*7iBe23Iw3T_S4+iZdoLyAs%K6Iqp#1Sn2`sU(P0Kva|p2#UjOcxg$S{PIa` zXt7RWH}cOzwg|n|HR!8CFc;Iriy&gD^VTsNOQod8B1|eqFX2zD6vcLTV7D zN^G4%3kn|jAPeT0BFED~N1T{K`r_LuY$D9@mD`ykCfZr!frp8Ru^lDtOwx7agXeol z=+Wg$g&cG0w-hoADRCVdKv!8dnNBXh27;SUx#aRQGY0bjAw(`eGg5j3J0NL5FWhOnWpnwih1;9q3Pwol)QNqFhhlgwc2A6kGN>AV|g--e42nR`aB=rP7mvFEF zMA~^FK)+SEzE%$eBajjBHDFC_kUsgV)Pub7-AegO{KM03nni-Q1Zji<%ss8&9qT3C#j#Hy2v`EosudhV?bRSebl1}k2Be{I;%R~VNE{0t zxiQ-tQzhV=PC#GPlbnO<92(UdIBXGRC8W-QgACzmAbp7303C4Gxm!nF(`}m z&RNpb`dis}Ed;w;iLP@g%ePFITNvFA5bXx57HwcURTNJnU5^8ZwYXibS~)-PJp^dM zpM0yYv7Wj_9P!#T+IJEkG_^hldC99nFzk>vp9~*RizQy#gv<-Wx>c4D9v=gylpIWX zkizN{p=y!oqg9a+MV{Xi?uVR`8=HhivX_Ah`yaoZ^b=C`p?xESZc?ac#i#3;xs z#%zktX2r^_mtH&o#sK`;vstNFeKw6+mu>1O0NKtZ15;R0(Hn&vX(m z064@~(^<4~sleXj$)CK>MtE(38$;t@%nZsPQ5dy{if6Bb#%IkSxzR|`s(*>7Vu?g< zD&{UgEBp|j%pm276k_V&K3vKp5hzo=0Yel+2X#Xh!-eiA=FdDh$HeCUIyzAPI@~kz6TEhk`!oR0I#?Yo}ul zAOOffOqoM#OtuA%cd8-U1ep7uY3^M!;Q9lZo26_h0Km0qZi<1F66@( zh>I66JpGdOssmR-?UsEDNP|K|U~E5m1g-~*x=xt3xgE4}hZnMvWXSkeS>if=08)kC z6xJ5?24tpwX)x9YZjT%MBD#tQh43Y(#NjY;!9o__uU8lbRibJFlKmAVR97b!pUOd{ z4SqC0+GA){nimE4BWSJ79fRNN4icJhcCx{cgfmtVGMqFsy0Hkj<#BZ2y40x#HeZ9s z>#u3k(wb>)^+?wV5wm&&orDT_!Wc&Vs%}kUjEt~P;z*d$5 z2Nd~IAsFTJ7y(5*x5(E7!Kmssy2YWBQf7~S0$^`+*Fdrl1lOcv0S0#voTF5Jzxf=M z7%LOaWfDAS2Rw3#;}=l;0h__GEJ>y;s?>dj13buF{nB70rsD&cY{TnY$u___x)#K_ zT~XK3Dm?!IBPvBd(kFw6c%VzXsXrp}+{-&Eo~93E;K;l7##X%px|s8WgbHn<^l=ukPGRaN?Ybc3@|PAPI#r1InK&iS2- zAVnTi$&DpfB1A7G;TZv~kUWmz^Ni7;X&}EJA1LnqtS}O_)~U}8mVR6kO(_99VNOQ8 z>2U;z!iH{!FT#)fwTK!+@_t;9W=!@0&kpFoC|_Kp5YFp!vqbk2mVio3J@r6e{@_v7 zi`Z}!&xlJ{PO{GBSBcWFd}QfE=MwPBiePV&bLN(}m1LAKYXDapoj7rLDN6{~F)Cv| zPB@ma!M|OVE^b}M;)+cwM5}(8wCJieU1E#Zm$C8Tv_x_b^PdW;7wCM4!;vG=x*QUZ zBA2s_ekUMZi7(*xeNicHTFwSJf_2IoHK3@pNO8ok1-oKoRCe z7qg-i^?t#4rT}Y+dt+cv;3~y@482%1uE&4t$d7Jl==gv8CE82M6wh2jHVw&&FE3%4 zHnq;B{MmnIa_`B3V*CnrAvD)f%@lD6Qqlpi^iqh+uU-jxL6OQeF(7caMH$w>uhGnK zbTw9viDkr67g#R)+(>=Qz_R^G9EJdupjQl#VPHAG!3dqmedl~f`28a{KFQXyqcF7`d z73oS1z$Ar!O)g(WJZg+&u}d}yL$iyIfD1Wryd5L#uFF}vwFQ&oVLTe5W(Pv8a89F3 z4wy;!qKtSGUb>Z1;!Th=iU7(WFUC_KZWvEF(-1cVZ%5v?kV@FKng)e-#7ghrfr@ls zA?pg6K^p+xU#4B3SphXed~z2Yd1hZh!v#P=tOfjAB?9srKGa1hev1#UpxMN7d`z0Ky(x{4%^9NeO zB|>oGSoLh62J_r-62Ul$Pp;P21P+mL4GAU1dh3mHfmnGBZA;?h-fQS|Ld%5FpdqJG zzcJ|5w1QFEJ3){P)G^y~mWz#`!+(%I4vk637yK$h|# z;4B{-z>$(36#z0}$p;1Sg%W4M*959Y?75yq3tK}>U%n;)H4S&`n1Bmd+PLZLkvuEC z7tl4n1{r)--Kz%!(bF4TGADu-5twJqQsc%R>Y5ftqEaJbWV0 zu-_?59zIDGJiQ*zurx*gTCe90rbs2k%NYmtsPo@dUbUoFX!kWi+MZv|8In(kh3y81 zD=mhQ34})?_%l@4H*jrl7-k%r|%9myI;f zlw;)C1o6SLO<{ms+225cm2eA(uXLar13yM3t^@4ICJMSy*LgQl*A#Mq%y#^x6*)-c z5E|L+Bt}Jf zX-q{)f{I4#;%Lb$L1pF&6(`ULfE|Y@yA2W|%EG=ee<4(7%(B8YhN8YD3xxm4ZKQ-k zZ6zb2Z^=#QTe3vo8*dM5Yq5+8X9@r^#}u&h?T{Y~_u}%BCvPj8-0xJ-xs{do1MS>O z0aY+D<1wa?v<;(?@`|l{8!1D$Ju4O4woy6@l=H+}+el7P+rrIpZ5#Yz9tfbkWP|)< znHYt&n2+W5_?++)F@S&&vGEQ~bEtUc4iZj+|J+MuBq9cFCzV5G_jYLkNwecUn4)Cd z1HK`Vfe1Q^|I{`P%}J^#*g=<+CxH;h@WuI-> zDGL0K!l_hN?gN%%EtKjW!aM#uS_rI*i9Q=LpN%Tx{3Ppq{+aTs2tTcn;0hv(*s*x( zZdznw!mGk@fK@1xcG3i*gP9~BOMuqbDZl91z(naRL`>Tf>vxhU;4wfvx|2kZ&U2sb zWM{#c$powC5zsl>bJ2AVEgqG(-V<6>DBHy0dq~0H%nAf3)=YMpM#RMAaC*kgZhlMsK*Psu{N|o$zj14+6qy` zJwS6nUW9&X0Cm#9(mMnAf`e@_lB7ol>N_s}2L zl`8?Rc#!sNSfy31lL3rTj|!kKQXh_8T51bL^Dd%}VrjQx(vx>4?H&Xq!7pO^9lnN{ zjt2ZiZ)lEC@zySq8I)1Gx6SWph6GJrnP`U_S4Ry#zH6i%jMAGbpDjQ>kV>0CY;w)l z@0MNwUM%Ce38i#5n*)Ce_E1(c3XNE?hmJF}(hu%2RvOsD1YU?-dex%@9Gxwy-rGhA zcR*q&>MnRafF07%XYZxvsQ%#`>}|vseOQO7K701k_SwIeaw_o7$O+jgvTVXH291>Fk^rD!{9*;x~2+#x1M1TGwdhpHvl+rjhOQY%a|q0h!-jGDnmam zAF7Lzb*7)D57kA>I@6EREBc2XVFeZ{Un{nla_S3ncTFFy9{djQbr*%X17UX%$8}LRo`!SXz8Mt1QJ;o|^Iq(=8 zlSmD&H<%I6{>+A30}?7edW=6Pxx!(Cw4`EL&l3IlwB0 zgb-qpub#ks6@rjIdPLQ0#Eb*1M7CNZu7z@vgd5!T@Z=S#;?MyWtFb;lpeK5*X`S4B}7OPksGm%YYs+Z7mxV33sRa-+zrQ zWbBvy&V%fmboe{|uWWH-L`0I<`8jKXzx)65IZMrq*<6=Cfc~@J@+)KqSf*ybf@G45 zS?Z%L5$V>L3no%xR^UQ$fi+?X>k^x+5fj)F@fv)MUP_gL9{4lWhW!VYEA)#H6a%2Z z#aN2P(j_9_7LgaX0s+YY=nbqCjkbucIhRqRz~xwxMr2k?$dw35C&*OZk3SR5mH2utpq6z+<~AW019kR!(uTOcVMXmOLvM7Ve9fo zSYLx}pTm-i+CDEj?GaTiFCe6WATJVn;AMnNC2Fro$ZH5`BFO6m3H|i))D{9CB5)~h zk4O{0*duaRACWEJ#Fnju@@ENo8zIvO@{WYOhY%k@-j|RM5#lGvM-uWeLfQ!OiG+NL zkh2N$R|)wHA=3%+xsu8+5Hy28UrMBZB4j2(j!DRIgv=ty2?;rgkaGy~ZwdJ?Le3?~ z*AntALS_@>JA#x3en8N901C!ci_G|l()=G0(hiWoPgt6VrJu1hA4|W8Iq?y>j$Z)) z(#|FUyaV7pfe0vqVhBVbXd%f<_W9T%8UdY9>J3;Fj%|`S7#}gNHU_H}BT_7uF2s@@ zOI=XXzwbG<8^Li1UV^1~EG>mnZy*7RJpYY?nyJgMCJAenV<{O+y;w@Y(nVNG#nQ!C zO2g76Q0fh&LlLAtS31bTxrSNl+O=t|3Udgj68pT7pzc$XJA2 zN04!oIQu#?F!g%+njq^ZVf`9{oFO4o5Hg$~XG(|@A!`YOOu*XKB4iywToO``kQ)fn zK#1PHwWk8I+Vf7;}q8T2RE|MxGZ_7Tl060#H_TM4pELN1nnR_tGp9C0$@{{bk|?uh^Z delta 27456 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%0D?iOrPle5Pi@mE?)2&I^!3< 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 Date: Wed, 24 Jul 2024 08:37:18 -0400 Subject: [PATCH 076/393] TUNIC: Missing slot data bugfix (#3628) * Fix certain items not being added to slot data * Change where items get added to slot data --- worlds/tunic/__init__.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 9b28d1d451a8..b3aa1e6a3479 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -160,9 +160,9 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: if new_cxn: cls.seed_groups[group]["plando"].value.append(cxn) - def create_item(self, name: str) -> TunicItem: + def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] - return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player) def create_items(self) -> None: @@ -192,14 +192,12 @@ def create_items(self) -> None: self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) elif self.options.laurels_location == "10_fairies": self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) - self.slot_data_items.append(laurels) items_to_create["Hero's Laurels"] = 0 if self.options.keys_behind_bosses: for rgb_hexagon, location in hexagon_locations.items(): 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 items_to_create[gold_hexagon] -= 3 @@ -245,33 +243,30 @@ 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) + tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) 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) + tunic_items.append(self.create_item(page, ItemClassification.useful)) 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) + tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 if self.options.lanternless: - lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) - tunic_items.append(lantern_item) + tunic_items.append(self.create_item("Lantern", ItemClassification.useful)) items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): 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) - tunic_items.append(tunic_item) + tunic_items.append(self.create_item(item)) + + for tunic_item in tunic_items: + if tunic_item.name in slot_data_item_names: + self.slot_data_items.append(tunic_item) self.multiworld.itempool += tunic_items From b23c1202582cbdf9a8e882d6b65cb580c5c5a382 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:17:43 -0400 Subject: [PATCH 077/393] Subnautica: Fix deprecated option getting (#3685) --- worlds/subnautica/rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/rules.py b/worlds/subnautica/rules.py index 3b6c5cd4dd68..ea9ec6a8058f 100644 --- a/worlds/subnautica/rules.py +++ b/worlds/subnautica/rules.py @@ -150,7 +150,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: def get_max_swim_depth(state: "CollectionState", player: int) -> int: - swim_rule: SwimRule = state.multiworld.swim_rule[player] + swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule depth: int = swim_rule.base_depth if swim_rule.consider_items: if has_seaglide(state, player): @@ -296,7 +296,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = multiworld.creature_scan_logic[player] + option = multiworld.worlds[player].options.creature_scan_logic for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(multiworld, player, creature_name) From 2307694012f3d49dbacbcdb059473f52c87aedea Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 24 Jul 2024 20:08:58 -0500 Subject: [PATCH 078/393] HK: fix remove issues failing collect/remove test (#3667) --- worlds/hk/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 78287305df5f..fbc6461f6aab 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -554,7 +554,8 @@ def remove(self, state, item: HKItem) -> bool: for effect_name, effect_value in item_effects.get(item.name, {}).items(): if state.prog_items[item.player][effect_name] == effect_value: del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + else: + state.prog_items[item.player][effect_name] -= effect_value return change From 697f7495184bbc7791ab3ad93bd2d7f4fa468ea5 Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:06:45 -0400 Subject: [PATCH 079/393] TUNIC: Missing slot data bugfix (#3628) * Fix certain items not being added to slot data * Change where items get added to slot data From 94e6e978f330c25eeca37692f03b52cbfeb4c386 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:07:20 -0400 Subject: [PATCH 080/393] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Also=20fix=20Rt?= =?UTF-8?q?=204=20Hidden=20Item=20(#3668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: alchav --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 251beb59cc18..6aee25df2637 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -427,7 +427,7 @@ def __init__(self, flag): LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items), LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items), LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), - LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), + LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), From f34da74012ba0f69433b158a68e008dc36a258d7 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 25 Jul 2024 00:13:16 -0400 Subject: [PATCH 081/393] Stardew Valley: Make Fairy Dust a Ginger Island only item and location (#3650) --- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/data/locations.csv | 2 +- worlds/stardew_valley/items.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 2604ad2c46bd..e026090f8659 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -307,7 +307,7 @@ id,name,classification,groups,mod_name 322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", -325,Fairy Dust Recipe,progression,, +325,Fairy Dust Recipe,progression,"GINGER_ISLAND", 326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 6e30d2b8c858..242d00b4455b 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2088,7 +2088,7 @@ id,region,name,tags,mod_name 3472,Farm,Craft Life Elixir,CRAFTSANITY, 3473,Farm,Craft Oil of Garlic,CRAFTSANITY, 3474,Farm,Craft Monster Musk,CRAFTSANITY, -3475,Farm,Craft Fairy Dust,CRAFTSANITY, +3475,Farm,Craft Fairy Dust,"CRAFTSANITY,GINGER_ISLAND", 3476,Farm,Craft Warp Totem: Beach,CRAFTSANITY, 3477,Farm,Craft Warp Totem: Mountains,CRAFTSANITY, 3478,Farm,Craft Warp Totem: Farm,CRAFTSANITY, diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index cb6102016942..31c7da5e3ade 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -409,8 +409,9 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star else: items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE items.append(item_factory(Wallet.iridium_snake_milk)) - items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: + items.append(item_factory("Fairy Dust Recipe")) def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): From 496f0e09afe7e863cf1bcce948d48bf591086274 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 01:21:51 -0500 Subject: [PATCH 082/393] CommonClient: forget password when disconnecting (#3641) * makes the kivy connect button do the same username forgetting that /connect does to fix an issue where losing connection would make you unable to connect to a different server * extract duplicate code * per request, adds handling on any disconnect to forget the saved password as to not leak it to other servers --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 2 ++ kvui.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index f8d1fcb7a221..09937e4b9ab8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -61,6 +61,7 @@ def _cmd_connect(self, address: str = "") -> bool: if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -514,6 +515,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]): async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() diff --git a/kvui.py b/kvui.py index a63d636960a7..f83590a819d5 100644 --- a/kvui.py +++ b/kvui.py @@ -596,6 +596,7 @@ def command_button_action(self, button): def connect_button_action(self, button): self.ctx.username = None + self.ctx.password = None if self.ctx.server: async_start(self.ctx.disconnect()) else: From deae524e9ba9f1bab1c48642f11108799d7b3b3a Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 02:05:04 -0500 Subject: [PATCH 083/393] Docs: add a living faq document for sharing dev solutions (#3156) * adding one faq :) * adding another faq that links to the relevant file * add lined line breaks between questions and lower the heading size of the question so sub-divisions can be added later * missed some newlines * updating best practice filler method * add note about get_filler_item_name() * updates to wording from review * add section to CODEOWNERS for maintainers of this doc * use underscores to reference the file easier in CODEOWNERS * update link to be direct and filter to function name --- docs/CODEOWNERS | 14 +++++++++++--- docs/apworld_dev_faq.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 docs/apworld_dev_faq.md diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 3b40d7e77a73..ab841e65ee4c 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,8 +1,8 @@ # Archipelago World Code Owners / Maintainers Document # -# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull -# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to -# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer. +# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as +# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in +# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly. # # All usernames must be GitHub usernames (and are case sensitive). @@ -226,3 +226,11 @@ # Ori and the Blind Forest # /worlds_disabled/oribf/ + +################### +## Documentation ## +################### + +# Apworld Dev Faq +/docs/apworld_dev_faq.md @qwint @ScipioWright + diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md new file mode 100644 index 000000000000..059c33844f27 --- /dev/null +++ b/docs/apworld_dev_faq.md @@ -0,0 +1,32 @@ +# APWorld Dev FAQ + +This document is meant as a reference tool to show solutions to common problems when developing an apworld. + +--- + +### My game has a restrictive start that leads to fill errors + +Hint to the Generator that an item needs to be in sphere one with local_early_items +```py +early_item_name = "Sword" +self.multiworld.local_early_items[self.player][early_item_name] = 1 +``` + +--- + +### I have multiple settings that change the item/location pool counts and need to balance them out + +In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible. + +If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit + +Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names +```py +total_locations = len(self.multiworld.get_unfilled_locations(self.player)) +item_pool = self.create_non_filler_items() + +while len(item_pool) < total_locations: + item_pool.append(self.create_filler()) + +self.multiworld.itempool += item_pool +``` From 8949e215654c29f3e46e254bd28fa8572c0acdbc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:10:36 +0200 Subject: [PATCH 084/393] settings: safer writing (#3644) * settings: clean up imports * settings: try to use atomic rename * settings: flush, sync and validate new yaml before replacing the old one * settings: add test for Settings.save --- settings.py | 18 +++++++++++++----- test/general/test_host_yaml.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/settings.py b/settings.py index 7ab618c344d8..792770521459 100644 --- a/settings.py +++ b/settings.py @@ -3,6 +3,7 @@ This is different from player options. """ +import os import os.path import shutil import sys @@ -11,7 +12,6 @@ from enum import IntEnum from threading import Lock from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar -import os __all__ = [ "get_settings", "fmt_doc", "no_gui", @@ -798,6 +798,7 @@ def autosave() -> None: atexit.register(autosave) def save(self, location: Optional[str] = None) -> None: # as above + from Utils import parse_yaml location = location or self._filename assert location, "No file specified" temp_location = location + ".tmp" # not using tempfile to test expected file access @@ -807,10 +808,18 @@ def save(self, location: Optional[str] = None) -> None: # as above # can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM with open(temp_location, "w", encoding="utf-8") as f: self.dump(f) - # replace old with new - if os.path.exists(location): + f.flush() + if hasattr(os, "fsync"): + os.fsync(f.fileno()) + # validate new file is valid yaml + with open(temp_location, encoding="utf-8") as f: + parse_yaml(f.read()) + # replace old with new, try atomic operation first + try: + os.rename(temp_location, location) + except (OSError, FileExistsError): os.unlink(location) - os.rename(temp_location, location) + os.rename(temp_location, location) self._filename = location def dump(self, f: TextIO, level: int = 0) -> None: @@ -832,7 +841,6 @@ def get_settings() -> Settings: with _lock: # make sure we only have one instance res = getattr(get_settings, "_cache", None) if not res: - import os from Utils import user_path, local_path filenames = ("options.yaml", "host.yaml") locations: List[str] = [] diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 7174befca428..3edbd34a51c5 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -1,11 +1,12 @@ import os +import os.path import unittest from io import StringIO -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from typing import Any, Dict, List, cast import Utils -from settings import Settings, Group +from settings import Group, Settings, ServerOptions class TestIDs(unittest.TestCase): @@ -80,3 +81,27 @@ class AGroup(Group): self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list self.assertGreater(value_spaces[3], value_spaces[0], f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}") + + +class TestSettingsSave(unittest.TestCase): + def test_save(self) -> None: + """Test that saving and updating works""" + with TemporaryDirectory() as d: + filename = os.path.join(d, "host.yaml") + new_release_mode = ServerOptions.ReleaseMode("enabled") + # create default host.yaml + settings = Settings(None) + settings.save(filename) + self.assertTrue(os.path.exists(filename), + "Default settings could not be saved") + self.assertNotEqual(settings.server_options.release_mode, new_release_mode, + "Unexpected default release mode") + # update host.yaml + settings.server_options.release_mode = new_release_mode + settings.save(filename) + self.assertFalse(os.path.exists(filename + ".tmp"), + "Temp file was not removed during save") + # read back host.yaml + settings = Settings(filename) + self.assertEqual(settings.server_options.release_mode, new_release_mode, + "Settings were not overwritten") From 205ca7fa37cbcfc1515eaace6239a1acbb34f6a2 Mon Sep 17 00:00:00 2001 From: Witchybun <96719127+Witchybun@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:22:46 -0500 Subject: [PATCH 085/393] Stardew Valley: Fix Daggerfish, Cropsanity; Move Some Rules to Content Packs; Add Missing Shipsanity Location (#3626) * Fix logic bug on daggerfish * Make new region for pond. * Fix SVE logic for crops * Fix Distant Lands Cropsanity * Fix failing tests. * Reverting removing these for now. * Fix bugs, add combat requirement * convert str into tuple directly * add ginger island to mod tests * Move a lot of mod item logic to content pack * Gut the rules from DL while we're at it. * Import nuke * Fix alecto * Move back some rules for now. * Move archaeology rules * Add some comments why its done. * Clean up archaeology and fix sve * Moved dulse to water item class * Remove digging like worms for now * fix * Add missing shipsanity location * Move background names around or something idk * Revert ArchaeologyTrash for now --------- Co-authored-by: Jouramie --- worlds/stardew_valley/content/mods/alecto.py | 33 +++++ .../stardew_valley/content/mods/archeology.py | 36 ++++-- .../content/mods/distant_lands.py | 31 ++++- .../stardew_valley/content/mods/npc_mods.py | 7 -- worlds/stardew_valley/content/mods/sve.py | 102 ++++++++++++++-- .../content/vanilla/pelican_town.py | 6 +- worlds/stardew_valley/data/fish_data.py | 7 +- worlds/stardew_valley/data/locations.csv | 3 +- worlds/stardew_valley/data/recipe_data.py | 4 +- worlds/stardew_valley/data/requirement.py | 21 ++++ worlds/stardew_valley/data/shop.py | 4 +- .../stardew_valley/logic/requirement_logic.py | 27 +++- .../stardew_valley/mods/logic/item_logic.py | 115 +----------------- worlds/stardew_valley/mods/mod_regions.py | 4 +- worlds/stardew_valley/rules.py | 1 + worlds/stardew_valley/strings/book_names.py | 4 - .../stardew_valley/strings/entrance_names.py | 1 + worlds/stardew_valley/strings/fish_names.py | 5 +- worlds/stardew_valley/strings/food_names.py | 1 + worlds/stardew_valley/strings/region_names.py | 1 + worlds/stardew_valley/test/mods/TestMods.py | 8 +- 21 files changed, 258 insertions(+), 163 deletions(-) create mode 100644 worlds/stardew_valley/content/mods/alecto.py diff --git a/worlds/stardew_valley/content/mods/alecto.py b/worlds/stardew_valley/content/mods/alecto.py new file mode 100644 index 000000000000..c05c936de3c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/alecto.py @@ -0,0 +1,33 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.seed_names import DistantLandsSeed + + +class AlectoContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + if ModNames.distant_lands in content.registered_packs: + content.game_items.pop(DistantLandsSeed.void_mint) + content.game_items.pop(DistantLandsSeed.vile_ancient_fruit) + content.source_item(DistantLandsSeed.void_mint, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),), + content.source_item(DistantLandsSeed.vile_ancient_fruit, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ), + + +register_mod_content_pack(ContentPack( + ModNames.alecto, + weak_dependencies=( + ModNames.distant_lands, # For Witch's order + ), + villagers=( + villagers_data.alecto, + ) + +)) diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py index 97d38085d3b2..5eb8af4cfc38 100644 --- a/worlds/stardew_valley/content/mods/archeology.py +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -1,20 +1,34 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack -from ...data.game_item import ItemTag, Tag -from ...data.shop import ShopSource +from ...data.artisan import MachineSource 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.craftable_names import ModMachine +from ...strings.fish_names import ModTrash +from ...strings.metal_names import all_artifacts, all_fossils from ...strings.skill_names import ModSkill -register_mod_content_pack(ContentPack( + +class ArchaeologyContentPack(ContentPack): + def artisan_good_hook(self, content: StardewContent): + # Done as honestly there are too many display items to put into the initial registration traditionally. + display_items = all_artifacts + all_fossils + for item in display_items: + self.source_display_items(item, content) + content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts)) + + def source_display_items(self, item: str, content: StardewContent): + wood_display = f"Wooden Display: {item}" + hardwood_display = f"Hardwood Display: {item}" + if item == "Trilobite": + wood_display = f"Wooden Display: Trilobite Fossil" + hardwood_display = f"Hardwood Display: Trilobite Fossil" + content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber)) + content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber)) + + +register_mod_content_pack(ArchaeologyContentPack( 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/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py index 19380d4ff565..c5614d130250 100644 --- a/worlds/stardew_valley/content/mods/distant_lands.py +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -1,9 +1,26 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import QuestRequirement from ...mods.mod_data import ModNames +from ...strings.crop_names import DistantLandsCrop +from ...strings.forageable_names import DistantLandsForageable +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import DistantLandsSeed -register_mod_content_pack(ContentPack( + +class DistantLandsContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED) + + +register_mod_content_pack(DistantLandsContentPack( ModNames.distant_lands, fishes=( fish_data.void_minnow, @@ -13,5 +30,13 @@ ), villagers=( villagers_data.zic, - ) + ), + harvest_sources={ + DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),), + DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),) + } )) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py index 3172a55dbf32..52d97d5c52b7 100644 --- a/worlds/stardew_valley/content/mods/npc_mods.py +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -73,13 +73,6 @@ ) )) -register_mod_content_pack(ContentPack( - ModNames.alecto, - villagers=( - villagers_data.alecto, - ) -)) - register_mod_content_pack(ContentPack( ModNames.lacey, villagers=( diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py index f74b80948c96..a68d4ae9c097 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -3,15 +3,27 @@ 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 ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement +from ...data.shop import ShopSource from ...mods.mod_data import ModNames -from ...strings.crop_names import Fruit -from ...strings.fish_names import WaterItem +from ...strings.craftable_names import ModEdible +from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit +from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem from ...strings.flower_names import Flower -from ...strings.forageable_names import Mushroom, Forageable -from ...strings.region_names import Region, SVERegion +from ...strings.food_names import SVEMeal, SVEBeverage +from ...strings.forageable_names import Mushroom, Forageable, SVEForage +from ...strings.gift_names import SVEGift +from ...strings.metal_names import Ore +from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.performance_names import Performance +from ...strings.region_names import Region, SVERegion, LogicRegion from ...strings.season_names import Season +from ...strings.seed_names import SVESeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial +from ...strings.villager_names import ModNPC class SVEContentPack(ContentPack): @@ -38,6 +50,24 @@ def villager_hook(self, content: StardewContent): # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge content.villagers.pop(villagers_data.lance.name) + def harvest_source_hook(self, content: StardewContent): + content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED) + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Highlands seeds as these are behind Lance existing. + content.game_items.pop(SVESeed.void) + content.game_items.pop(SVEVegetable.void_root) + content.game_items.pop(SVESeed.stalk) + content.game_items.pop(SVEFruit.monster_fruit) + content.game_items.pop(SVESeed.fungus) + content.game_items.pop(SVEVegetable.monster_mushroom) + content.game_items.pop(SVESeed.slime) + content.game_items.pop(SVEFruit.slime_berry) + register_mod_content_pack(SVEContentPack( ModNames.sve, @@ -45,12 +75,24 @@ def villager_hook(self, content: StardewContent): ginger_island_content_pack.name, ModNames.jasper, # To override Marlon and Gunther ), + shop_sources={ + SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),), + SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),), + ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), + SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), + ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), + SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), + SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),), + }, 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,), ) + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) ), Mushroom.morel: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) @@ -64,17 +106,59 @@ def villager_hook(self, content: StardewContent): 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,), seasons=Season.not_winter, 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),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ), + # New items + + ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), + ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), + other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), + ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), + SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),), + SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),), + SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),), + SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),), + SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),), + SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),), + SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),), + SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),), + SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),), + SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),), + SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),), + SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),), + SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), + ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), + other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), + # 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,)),), + + # Crops + SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),), + SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),), + SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),), + SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),), + SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),), + SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),), + SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),), + }, fishes=( fish_data.baby_lunaloo, # Removed when no ginger island diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 917e8cca220a..220b46eae2a4 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -229,7 +229,7 @@ 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), + GenericSource(regions=(Region.adventurer_guild_bedroom,)), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), @@ -243,12 +243,12 @@ ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), Book.the_alleyway_buffet: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.town, + 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, + GenericSource(regions=(Region.beach,), other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), SkillRequirement(Skill.fishing, 6), SeasonRequirement(Season.winter))), diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index c6f0c30d41ff..26b1a0d58a81 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -46,7 +46,8 @@ def __repr__(self): crimson_badlands = (SVERegion.crimson_badlands,) shearwater = (SVERegion.shearwater,) -highlands = (SVERegion.highlands_outside,) +highlands_pond = (SVERegion.highlands_pond,) +highlands_cave = (SVERegion.highlands_cavern,) sprite_spring = (SVERegion.sprite_spring,) fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) @@ -133,9 +134,9 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve) butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve) clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve) -daggerfish = create_fish(SVEFish.daggerfish, highlands, season.all_seasons, 50, mod_name=ModNames.sve) +daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve) frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve) -gemfish = create_fish(SVEFish.gemfish, highlands, season.all_seasons, 100, mod_name=ModNames.sve) +gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve) goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve) grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve) king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 242d00b4455b..0d7a10f95496 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2900,7 +2900,6 @@ 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 @@ -3280,10 +3279,10 @@ id,region,name,tags,mod_name 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 +8247,Shipping,Shipsanity: Snatcher Worm,SHIPSANITY,Stardew Valley Expanded diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index b48246876271..3123bb924307 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -5,7 +5,7 @@ from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop -from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish +from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient @@ -195,7 +195,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEFish.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 4744f9dffdfe..b2416d8d0b72 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -31,6 +31,27 @@ class YearRequirement(Requirement): year: int +@dataclass(frozen=True) +class CombatRequirement(Requirement): + level: str + + +@dataclass(frozen=True) +class QuestRequirement(Requirement): + quest: str + + +@dataclass(frozen=True) +class RelationshipRequirement(Requirement): + npc: str + hearts: int + + +@dataclass(frozen=True) +class FishingRequirement(Requirement): + region: str + + @dataclass(frozen=True) class WalnutRequirement(Requirement): amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index ca54d35e14f2..f14dbac82131 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -16,8 +16,8 @@ class ShopSource(ItemSource): 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." + assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." + assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." @dataclass(frozen=True, **kw_only) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 9356440ac6a8..6a5adf4890c9 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -3,15 +3,20 @@ from .base_logic import BaseLogicMixin, BaseLogic from .book_logic import BookLogicMixin +from .combat_logic import CombatLogicMixin +from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin +from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin +from .relationship_logic import RelationshipLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ + RelationshipRequirement, FishingRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -21,7 +26,7 @@ def __init__(self, *args, **kwargs): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -55,3 +60,21 @@ def _(self, requirement: YearRequirement): @meet_requirement.register def _(self, requirement: WalnutRequirement): return self.logic.walnut.has_walnut(requirement.amount) + + @meet_requirement.register + def _(self, requirement: CombatRequirement): + return self.logic.combat.can_fight_at_level(requirement.level) + + @meet_requirement.register + def _(self, requirement: QuestRequirement): + return self.logic.quest.can_complete_quest(requirement.quest) + + @meet_requirement.register + def _(self, requirement: RelationshipRequirement): + return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts) + + @meet_requirement.register + def _(self, requirement: FishingRequirement): + return self.logic.fishing.can_fish_at(requirement.region) + + diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index cfafc88e83f5..ef5eab0134d1 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -23,24 +23,15 @@ from ...options import Cropsanity 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 -from ...strings.fish_names import ModTrash, SVEFish -from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable -from ...strings.gift_names import SVEGift +from ...strings.craftable_names import ModCraftable, ModMachine +from ...strings.fish_names import ModTrash from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import Loot from ...strings.performance_names import Performance -from ...strings.quest_names import ModQuest -from ...strings.region_names import Region, SVERegion, DeepWoodsRegion, BoardingHouseRegion -from ...strings.season_names import Season -from ...strings.seed_names import SVESeed, DistantLandsSeed -from ...strings.skill_names import Skill +from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion from ...strings.tool_names import Tool, ToolMaterial -from ...strings.villager_names import ModNPC display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display] display_items = all_artifacts + all_fossils @@ -58,12 +49,6 @@ class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, Cooking def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() - if ModNames.sve in self.options.mods: - items.update(self.get_sve_item_rules()) - if ModNames.archaeology in self.options.mods: - items.update(self.get_archaeology_item_rules()) - if ModNames.distant_lands in self.options.mods: - items.update(self.get_distant_lands_item_rules()) if ModNames.boarding_house in self.options.mods: items.update(self.get_boarding_house_item_rules()) return items @@ -75,61 +60,6 @@ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, Star item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule)) return item_rule - 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: 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), - 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), - 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.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: 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_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.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.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.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), - 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 - } - # @formatter:on - def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): return { Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( @@ -141,7 +71,7 @@ def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): 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]): @@ -160,36 +90,6 @@ def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): return options_to_update - def get_archaeology_item_rules(self): - archaeology_item_rules = {} - preservation_chamber_rule = self.logic.has(ModMachine.preservation_chamber) - hardwood_preservation_chamber_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) - for item in display_items: - for display_type in display_types: - if item == "Trilobite": - location_name = f"{display_type}: Trilobite Fossil" - else: - location_name = f"{display_type}: {item}" - display_item_rule = self.logic.crafting.can_craft(all_crafting_recipes_by_name[display_type]) & self.logic.has(item) - if "Wooden" in display_type: - 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): - return { - DistantLandsForageable.swamp_herb: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsForageable.brown_amanita: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsSeed.vile_ancient_fruit: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsSeed.void_mint: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsCrop.void_mint: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.void_mint), - DistantLandsCrop.vile_ancient_fruit: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.vile_ancient_fruit), - } - def get_boarding_house_item_rules(self): return { # Mob Drops from lost valley enemies @@ -251,8 +151,3 @@ def get_boarding_house_item_rules(self): BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level( Performance.great), } - - def has_seed_unlocked(self, seed_name: str): - if self.options.cropsanity == Cropsanity.option_disabled: - return True_() - return self.logic.received(seed_name) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index c075bd4d106f..a402ba606868 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -183,7 +183,8 @@ 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_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True), + RegionData(SVERegion.highlands_pond, 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), @@ -276,6 +277,7 @@ 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), + ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ] alecto_regions = [ diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7c1fdbda3cf4..89b1cf87c3c1 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1031,6 +1031,7 @@ def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, pla 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)) + set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py index 3c32cd81b326..6c271f42ae9c 100644 --- a/worlds/stardew_valley/strings/book_names.py +++ b/worlds/stardew_valley/strings/book_names.py @@ -27,10 +27,6 @@ class Book: the_diamond_hunter = "The Diamond Hunter" -class ModBook: - digging_like_worms = "Digging Like Worms" - - ordered_lost_books = [] all_lost_books = set() diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 9b651f42760a..58a919f2a8a4 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -358,6 +358,7 @@ class SVEEntrance: sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave" fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom" museum_to_gunther_bedroom = "Museum to Gunther's Bedroom" + highlands_to_pond = "Highlands to Highlands Pond" class AlectoEntrance: diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index d94f9e2fd403..d4ee81430eb4 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -137,7 +137,6 @@ class SVEFish: void_eel = "Void Eel" water_grub = "Water Grub" sea_sponge = "Sea Sponge" - dulse_seaweed = "Dulse Seaweed" class DistantLandsFish: @@ -147,6 +146,10 @@ class DistantLandsFish: giant_horsehoe_crab = "Giant Horsehoe Crab" +class SVEWaterItem: + dulse_seaweed = "Dulse Seaweed" + + class ModTrash: rusty_scrap = "Scrap Rust" diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 5555316f8314..03784336d19c 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -102,6 +102,7 @@ class SVEMeal: void_delight = "Void Delight" void_salmon_sushi = "Void Salmon Sushi" grampleton_orange_chicken = "Grampleton Orange Chicken" + stamina_capsule = "Stamina Capsule" class TrashyMeal: diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 9cedb6b8ef32..58763b6fcb80 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -296,6 +296,7 @@ class SVERegion: sprite_spring_cave = "Sprite Spring Cave" willy_bedroom = "Willy's Bedroom" gunther_bedroom = "Gunther's Bedroom" + highlands_pond = "Highlands Pond" class AlectoRegion: diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 5e7e9d4143bd..97184b1338b8 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -14,7 +14,8 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in options.Mods.valid_keys: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): + world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false} + with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) @@ -22,8 +23,9 @@ def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basi for option in options.EntranceRandomization.options: for mod in options.Mods.valid_keys: world_options = { - options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], - options.Mods: mod + options.EntranceRandomization: options.EntranceRandomization.options[option], + options.Mods: mod, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false } with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) From b019485944543e8b1cb440c572f27b6a592a0dd8 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Thu, 25 Jul 2024 03:27:22 -0400 Subject: [PATCH 086/393] AHIT: Update Setup Guide (#3647) --- worlds/ahit/docs/setup_en.md | 63 ++++++++---------------------------- 1 file changed, 13 insertions(+), 50 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 509869fc256a..23b34907071c 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -12,41 +12,29 @@ ## Instructions -1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) -This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, -paste the link into the box, and hit Enter. +1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!** + Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place. + **This is important! Changing the game version CAN and WILL break your existing save files!!!** -2. In the Steam console, enter the following command: -`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, -**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** -or else the download may potentially become corrupted (see first FAQ issue below). +2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**. -3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. +3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. + While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) -4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. +4. Once the game finishes downloading, start it up. + In Game Settings, make sure **Enable Developer Console** is checked. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. -In this new text file, input the number **253230** on the first line. - - -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. -You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. - - -7. Start up the game using your new shortcut. To confirm if you are on the correct version, -go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running -the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. +5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** -(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher +and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. @@ -61,33 +49,8 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t ## FAQ/Common Issues -### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! -If you receive an error message such as -**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or -**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot -download was likely corrupted. The only way to fix this is to start the entire download all over again. -Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this -from happening is to ensure that your connection is not interrupted or slowed while downloading. - -### The game keeps crashing on startup after the splash screen! -This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, -try the following: - -- Close Steam **entirely**. -- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. -- Close the game, and then open Steam again. -- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. - -### I followed the setup, but "Live Game Events" still shows up in the options menu! -The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by -default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file -extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. -To show file extensions in Windows 10, open any folder, click the View tab at the top, and check -"File name extensions". Then you can correct the name of the file. If the name of the file is correct, -and you're still running into the issue, re-read the setup guide again in case you missed a step. -If you still can't get it to work, ask for help in the Discord thread. - -### The game is running on the older version, but it's not connecting when starting a new save! + +### The game is not connecting when starting a new save! For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu (rocket icon) in-game, and re-enable the mod. From 5fb1ebdcfd4a9adaaf7d655492069649c9de0482 Mon Sep 17 00:00:00 2001 From: Tsukino <16899482+Tsukino-uwu@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:30:23 +0200 Subject: [PATCH 087/393] Docs: Add Swedish Guide for Pokemon Emerald (#3252) * Docs: Add Swedish Guide for Pokemon Emerald Swedish Translation * v2 some proof reading & clarification changes * v3 * v4 * v5 typo * v6 * Update worlds/pokemon_emerald/docs/setup_sv.md Co-authored-by: Bryce Wilson * Update worlds/pokemon_emerald/docs/setup_sv.md Co-authored-by: Bryce Wilson * v7 Tried to reduce the length of lines, this should still convey the same message/meaning * typo * v8 Removed Leading/Trailing Spaces * typo v2 * Added a couple of full stops. * lowercase typos * Update setup_sv.md * Apply suggestions from code review Co-authored-by: Bryce Wilson --------- Co-authored-by: Bryce Wilson Co-authored-by: bittersweetrin --- worlds/pokemon_emerald/__init__.py | 11 +++- worlds/pokemon_emerald/docs/setup_sv.md | 78 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 worlds/pokemon_emerald/docs/setup_sv.md diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index aa4f6ccf7519..abdee26f572f 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -52,8 +52,17 @@ class PokemonEmeraldWebWorld(WebWorld): "setup/es", ["nachocua"] ) + + setup_sv = Tutorial( + "Multivärld Installations Guide", + "En guide för att kunna spela Pokémon Emerald med Archipelago.", + "Svenska", + "setup_sv.md", + "setup/sv", + ["Tsukino"] + ) - tutorials = [setup_en, setup_es] + tutorials = [setup_en, setup_es, setup_sv] class PokemonEmeraldSettings(settings.Group): diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md new file mode 100644 index 000000000000..88b1d384096b --- /dev/null +++ b/worlds/pokemon_emerald/docs/setup_sv.md @@ -0,0 +1,78 @@ +# Pokémon Emerald Installationsguide + +## Programvara som behövs + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Ett engelskt Pokémon Emerald ROM, Archipelago kan inte hjälpa dig med detta. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare + +### Konfigurera BizHawk + +När du har installerat BizHawk, öppna `EmuHawk.exe` och ändra följande inställningar: + +- Om du använder BizHawk 2.7 eller 2.8, gå till `Config > Customize`. På "Advanced Tab", byt Lua core från +`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efteråt. (Använder du BizHawk 2.9, kan du skippa detta steg.) +- Gå till `Config > Customize`. Markera "Run in background" inställningen för att förhindra bortkoppling från +klienten om du alt-tabbar bort från EmuHawk. +- Öppna en `.gba` fil i EmuHawk och gå till `Config > Controllers…` för att konfigurera dina inputs. +Om du inte hittar `Controllers…`, starta ett valfritt `.gba` ROM först. +- Överväg att rensa keybinds i `Config > Hotkeys…` som du inte tänkt använda. Välj en keybind och tryck på ESC +för att rensa bort den. + +## Extra programvara + +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), +används tillsammans med +[PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Generera och patcha ett spel + +1. Skapa din konfigurationsfil (YAML). Du kan göra en via att använda +[Pokémon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options). +2. Följ de allmänna Archipelago instruktionerna för att +[Generera ett spel](../../Archipelago/setup/en#generating-a-game). +Detta kommer generera en fil för dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg. +3. Öppna `ArchipelagoLauncher.exe` +4. Välj "Open Patch" på vänstra sidan, och välj din patchfil. +5. Om detta är första gången du patchar, så kommer du behöva välja var ditt ursprungliga ROM är. +6. En patchad `.gba` fil kommer skapas på samma plats som patchfilen. +7. Första gången du öppnar en patch med BizHawk-klienten, kommer du också behöva bekräfta var `EmuHawk.exe` filen är +installerad i din BizHawk-mapp. + +Om du bara tänkt spela själv och du inte bryr dig om automatisk spårning eller ledtrådar, så kan du stanna här, stänga +av klienten, och starta ditt patchade ROM med valfri emulator. Dock, för multvärldsfunktionen eller andra +Archipelago-funktioner, fortsätt nedanför med BizHawk. + +## Anslut till en server + +Om du vanligtsvis öppnar en patchad fil så görs steg 1-5 automatiskt åt dig. Även om det är så, kom ihåg dessa steg +ifall du till exempel behöver stänga ner och starta om något medans du spelar. + +1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel, +så kan du bara öppna den igen från launchern. +2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen. +3. I EmuHawk, gå till `Tools > Lua Console`. Luakonsolen måste vara igång medans du spelar. +4. I Luakonsolen, Tryck på `Script > Open Script…`. +5. Leta reda på din Archipelago-mapp och i den öppna `data/lua/connector_bizhawk_generic.lua`. +6. Emulatorn och klienten kommer så småningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är +anslutet och att Pokemon Emerald är igenkänt. +7. För att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex. +`archipelago.gg:38281` +längst upp i din klient och tryck sen på "Connect". + +Du borde nu kunna ta emot och skicka föremål. Du behöver göra dom här stegen varje gång du vill ansluta igen. Det är +helt okej att göra saker offline utan att behöva oroa sig; allt kommer att synkronisera när du ansluter till servern +igen. + +## Automatisk Spårning + +Pokémon Emerald har en fullt fungerande spårare med stöd för automatisk spårning. + +1. Ladda ner [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) +och +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat. +3. Öppna PopTracker, och välj Pokemon Emerald. +4. För att automatiskt spåra, tryck på "AP" symbolen längst upp. +5. Skriv in Archipelago-serverns uppgifter (Samma som du använde för att ansluta med klienten), "Slot"-namn samt +lösenord. From 79843803cf3a2547390f6e139be9c229a77d370b Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 16:01:22 -0500 Subject: [PATCH 088/393] Docs: Add header to FAQ doc referencing other relevant docs (#3692) * Add header to FAQ doc referencing other relevant docs * Update docs/apworld_dev_faq.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update docs/apworld_dev_faq.md Co-authored-by: Scipio Wright --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Scipio Wright --- docs/apworld_dev_faq.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md index 059c33844f27..71e2e4152e71 100644 --- a/docs/apworld_dev_faq.md +++ b/docs/apworld_dev_faq.md @@ -1,6 +1,8 @@ # APWorld Dev FAQ This document is meant as a reference tool to show solutions to common problems when developing an apworld. +It is not intended to answer every question about Archipelago and it assumes you have read the other docs, +including [Contributing](contributing.md), [Adding Games](), and [World API](). --- From b6e5223aa27bd77217897bcad41645b6645a6969 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:02:25 -0400 Subject: [PATCH 089/393] Docs: Expanding on the answers in the FAQ (#3690) * Expand on some existing answers * Oops * Sphere "one" * Removing while * Update docs/apworld_dev_faq.md Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- docs/apworld_dev_faq.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md index 71e2e4152e71..8d9429afa321 100644 --- a/docs/apworld_dev_faq.md +++ b/docs/apworld_dev_faq.md @@ -8,12 +8,18 @@ including [Contributing](contributing.md), [Adding Games](), an ### My game has a restrictive start that leads to fill errors -Hint to the Generator that an item needs to be in sphere one with local_early_items +Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one. ```py early_item_name = "Sword" self.multiworld.local_early_items[self.player][early_item_name] = 1 ``` +Some alternative ways to try to fix this problem are: +* Add more locations to sphere one of your world, potentially only when there would be a restrictive start +* Pre-place items yourself, such as during `create_items` +* Put items into the player's starting inventory using `push_precollected` +* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start + --- ### I have multiple settings that change the item/location pool counts and need to balance them out @@ -27,8 +33,13 @@ Note: to use self.create_filler(), self.get_filler_item_name() should be defined total_locations = len(self.multiworld.get_unfilled_locations(self.player)) item_pool = self.create_non_filler_items() -while len(item_pool) < total_locations: +for _ in range(total_locations - len(item_pool)): item_pool.append(self.create_filler()) self.multiworld.itempool += item_pool ``` + +A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions): +```py +item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))] +``` From d030a698a6824ec960fdcba40383d38991f82812 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 25 Jul 2024 17:09:37 -0400 Subject: [PATCH 090/393] Lingo: Changed minimum progression requirement (#3672) --- worlds/lingo/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3b67617873c7..a1b8b7c1d439 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,7 +9,7 @@ from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions, lingo_option_groups +from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -54,14 +54,17 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): - if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps): + if not (self.options.shuffle_doors or self.options.shuffle_colors or + (self.options.sunwarp_access >= SunwarpAccess.option_unlock and + self.options.victory_condition == VictoryCondition.option_pilgrimage)): if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" - f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem" - f" right.") + warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door" + f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition" + f" if that doesn't seem right.") else: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" - f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.") + raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on" + f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage" + f" victory condition.") self.player_logic = LingoPlayerLogic(self) From cc2216164489f89d78ac1e53aaa71b9dce04ac28 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 26 Jul 2024 04:53:11 -0400 Subject: [PATCH 091/393] Lingo: Add panels mode door shuffle (#3163) * Created panels mode door shuffle * Added some panel door item names * Remove RUNT TURN panel door Not really useful. * Fix logic with First SIX related stuff * Add group_doors to slot data * Fix LEVEL 2 behavior with panels mode * Fixed unit tests * Fixed duplicate IDs from merge * Just regenerated new IDs * Fixed duplication of color and door group items * Removed unnecessary unit test option * Fix The Seeker being achievable without entrance door * Fix The Observant being achievable without locked panels * Added some more panel doors * Added Progressive Suits Area * Lingo: Fix Basement access with THE MASTER * Added indirect conditions for MASTER-blocked entrances * Fixed Incomparable achievement access * Fix STAIRS panel logic * Fix merge error with good items * Is this clearer? * DREAD and TURN LEARN * Allow a weird edge case for reduced locations Panels mode door shuffle + grouped doors + color shuffle + pilgrimage enabled is exactly the right number of items for reduced locations. Removing color shuffle also allows for disabling pilgrimage, adding sunwarp locking, or both, with a couple of locations left over. * Prevent small sphere one on panels mode * Added shuffle_doors aliases for old options * Fixed a unit test * Updated datafile * Tweaked requirements for reduced locations * Added player name to OptionError messages * Update generated.dat --- worlds/lingo/__init__.py | 3 +- worlds/lingo/data/LL1.yaml | 694 +++++++++++++++++++++-- worlds/lingo/data/generated.dat | Bin 136563 -> 148903 bytes worlds/lingo/data/ids.yaml | 142 +++++ worlds/lingo/datatypes.py | 11 + worlds/lingo/items.py | 17 +- worlds/lingo/options.py | 29 +- worlds/lingo/player_logic.py | 121 +++- worlds/lingo/rules.py | 10 +- worlds/lingo/static_logic.py | 32 +- worlds/lingo/test/TestDoors.py | 56 +- worlds/lingo/test/TestOptions.py | 17 +- worlds/lingo/test/TestOrangeTower.py | 2 +- worlds/lingo/test/TestPanelsanity.py | 2 +- worlds/lingo/test/TestPilgrimage.py | 8 +- worlds/lingo/test/TestProgressive.py | 7 +- worlds/lingo/test/TestSunwarps.py | 21 +- worlds/lingo/utils/assign_ids.rb | 40 ++ worlds/lingo/utils/pickle_static_data.py | 124 +++- worlds/lingo/utils/validate_config.rb | 88 ++- 20 files changed, 1274 insertions(+), 150 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index a1b8b7c1d439..9853be73fa9b 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -170,7 +170,8 @@ def fill_slot_data(self): slot_options = [ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks", - "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps" + "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps", + "group_doors" ] slot_data = { diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 3035446ef793..1c9f4e551df1 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1,6 +1,13 @@ --- # This file is an associative array where the keys are region names. Rooms - # have four properties: entrances, panels, doors, and paintings. + # have a number of properties: + # - entrances + # - panels + # - doors + # - panel_doors + # - paintings + # - progression + # - sunwarps # # entrances is an array of regions from which this room can be accessed. The # key of each entry is the room that can access this one. The value is a list @@ -13,7 +20,7 @@ # room that the door is in. The room name may be omitted if the door is # located in the current room. # - # panels is an array of panels in the room. The key of the array is an + # panels is a named array of panels in the room. The key of the array is an # arbitrary name for the panel. Panels can have the following fields: # - id: The internal ID of the panel in the LINGO map # - required_room: In addition to having access to this room, the player must @@ -45,7 +52,7 @@ # - hunt: If True, the tracker will show this panel even when it is # not a check. Used for hunts like the Number Hunt. # - # doors is an array of doors associated with this room. When door + # doors is a named array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that # will be displayed as part of the item's name. Doors can have the following # fields: @@ -78,6 +85,18 @@ # - event: Denotes that the door is event only. This is similar to # setting both skip_location and skip_item. # + # panel_doors is a named array of "panel doors" associated with this room. + # When panel door shuffle is enabled, each of these becomes an item, and those + # items block access to the listed panels. The key is a name for internal + # reference only. Panel doors can have the following fields: + # - panels: Required. This is the set of panels that are blocked by this + # panel door. + # - item_name: Overrides the name of the item generated for this panel + # door. If not specified, the item name will be generated from + # the room name and the name(s) of the panel(s). + # - panel_group: When region grouping is enabled, all panel doors with the + # same group will be covered by a single item. + # # paintings is an array of paintings in the room. This is used for painting # shuffling. # - id: The internal painting ID from the LINGO map. @@ -105,6 +124,14 @@ # fine in door shuffle mode. # - move: Denotes that the painting is able to move. # + # progression is a named array of items that define an ordered set of items. + # progression items do not have any true connection to the rooms that they + # are defined in, but it is best to place them in a thematically appropriate + # room. The key for a progression entry is the name of the item that will be + # created. A progression entry is a dictionary with one or both of a "doors" + # key and a "panel_doors" key. These fields should be lists of doors or + # panel doors that will be contained in this progressive item. + # # sunwarps is an array of sunwarps in the room. This is used for sunwarp # shuffling. # - dots: The number of dots on this sunwarp. @@ -193,6 +220,10 @@ panel: RACECAR (Black) - room: The Tenacious panel: SOLOS (Black) + panel_doors: + HIDDEN: + panels: + - HIDDEN paintings: - id: arrows_painting exit_only: True @@ -303,6 +334,10 @@ panel: SOLOS (Black) - room: Hub Room panel: RAT + panel_doors: + OPEN: + panels: + - OPEN paintings: - id: owl_painting orientation: north @@ -317,7 +352,13 @@ panels: Achievement: id: Countdown Panels/Panel_seeker_seeker - required_room: Hidden Room + # The Seeker uniquely has the property that 1) it can be entered (through the Pilgrim Room) without opening the + # front door in panels mode door shuffle, and 2) the front door panel is part of the CDP. This necessitates this + # required_panel clause, because the entrance panel needs to be solvable for the achievement even if an + # alternate entrance to the room is used. + required_panel: + room: Hidden Room + panel: OPEN tag: forbid check: True achievement: The Seeker @@ -537,6 +578,23 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + ORDER: + panels: + - ORDER + SLAUGHTER: + panel_group: Tenacious Entrance Panels + panels: + - SLAUGHTER + TRACE: + panels: + - TRACE + RAT: + panels: + - RAT + OPEN: + panels: + - OPEN paintings: - id: maze_painting orientation: west @@ -608,12 +666,13 @@ item_name: "6 Sunwarp" progression: Progressive Pilgrimage: - - 1 Sunwarp - - 2 Sunwarp - - 3 Sunwarp - - 4 Sunwarp - - 5 Sunwarp - - 6 Sunwarp + doors: + - 1 Sunwarp + - 2 Sunwarp + - 3 Sunwarp + - 4 Sunwarp + - 5 Sunwarp + - 6 Sunwarp Pilgrim Antechamber: # The entrances to this room are special. When pilgrimage is enabled, we use a special access rule to determine # whether a pilgrimage can succeed. When pilgrimage is disabled, the sun painting will be added to the pool. @@ -881,6 +940,24 @@ panel: READS + RUST - room: Ending Area panel: THE END + panel_doors: + DECAY: + panel_group: Tenacious Entrance Panels + panels: + - DECAY + NOPE: + panels: + - NOPE + WE ROT: + panels: + - WE ROT + WORDS SWORD: + panels: + - WORDS + - SWORD + BEND HI: + panels: + - BEND HI paintings: - id: eye_painting disable: True @@ -895,6 +972,14 @@ direction: exit entrance_indicator_pos: [ -17, 2.5, -41.01 ] orientation: north + progression: + Progressive Suits Area: + panel_doors: + - WORDS SWORD + - room: Lost Area + panel_door: LOST + - room: Amen Name Area + panel_door: AMEN NAME Lost Area: entrances: Outside The Agreeable: @@ -920,6 +1005,11 @@ panels: - LOST (1) - LOST (2) + panel_doors: + LOST: + panels: + - LOST (1) + - LOST (2) Amen Name Area: entrances: Crossroads: @@ -953,6 +1043,11 @@ panels: - AMEN - NAME + panel_doors: + AMEN NAME: + panels: + - AMEN + - NAME Suits Area: entrances: Amen Name Area: @@ -1056,6 +1151,13 @@ - LEVEL (White) - RACECAR (White) - SOLOS (White) + panel_doors: + Black Palindromes: + item_name: The Tenacious - Black Palindromes (Panels) + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) Near Far Area: entrances: Hub Room: True @@ -1081,6 +1183,21 @@ panels: - NEAR - FAR + panel_doors: + NEAR FAR: + item_name: Symmetry Room - NEAR, FAR (Panels) + panel_group: Symmetry Room Panels + panels: + - NEAR + - FAR + progression: + Progressive Symmetry Room: + panel_doors: + - NEAR FAR + - room: Warts Straw Area + panel_door: WARTS STRAW + - room: Leaf Feel Area + panel_door: LEAF FEEL Warts Straw Area: entrances: Near Far Area: @@ -1108,6 +1225,13 @@ panels: - WARTS - STRAW + panel_doors: + WARTS STRAW: + item_name: Symmetry Room - WARTS, STRAW (Panels) + panel_group: Symmetry Room Panels + panels: + - WARTS + - STRAW Leaf Feel Area: entrances: Warts Straw Area: @@ -1135,6 +1259,13 @@ panels: - LEAF - FEEL + panel_doors: + LEAF FEEL: + item_name: Symmetry Room - LEAF, FEEL (Panels) + panel_group: Symmetry Room Panels + panels: + - LEAF + - FEEL Outside The Agreeable: entrances: Crossroads: @@ -1243,6 +1374,20 @@ panels: - room: Color Hunt panel: PURPLE + panel_doors: + MASSACRED: + panel_group: Tenacious Entrance Panels + panels: + - MASSACRED + BLACK: + panels: + - BLACK + CLOSE: + panels: + - CLOSE + RIGHT: + panels: + - RIGHT paintings: - id: eyes_yellow_painting orientation: east @@ -1294,6 +1439,14 @@ - WINTER - DIAMONDS - FIRE + panel_doors: + Lookout: + item_name: Compass Room Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: pencil_painting7 orientation: north @@ -1510,6 +1663,10 @@ - HIDE (3) - room: Outside The Agreeable panel: HIDE + panel_doors: + DOWN: + panels: + - DOWN The Perceptive: entrances: Starting Room: @@ -1531,6 +1688,10 @@ check: True exclude_reduce: True tag: botwhite + panel_doors: + GAZE: + panels: + - GAZE paintings: - id: garden_painting_tower orientation: north @@ -1572,9 +1733,10 @@ - EAT progression: Progressive Fearless: - - Second Floor - - room: The Fearless (Second Floor) - door: Third Floor + doors: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor The Fearless (Second Floor): entrances: The Fearless (First Floor): @@ -1669,6 +1831,10 @@ tag: forbid required_door: door: Stairs + required_panel: + - panel: FOUR (1) + - panel: FOUR (2) + - panel: SIX achievement: The Observant FOUR (1): id: Look Room/Panel_four_back @@ -1782,6 +1948,16 @@ door_group: Observant Doors panels: - SIX + panel_doors: + BACKSIDE: + item_name: The Observant - Backside Entrance Panels + panel_group: Backside Entrance Panels + panels: + - FOUR (1) + - FOUR (2) + STAIRS: + panels: + - SIX The Incomparable: entrances: The Observant: @@ -1798,9 +1974,12 @@ check: True tag: forbid required_room: - - Elements Area - - Courtyard - Eight Room + required_panel: + - room: Courtyard + panel: I + - room: Elements Area + panel: A achievement: The Incomparable A (One): id: Strand Room/Panel_blank_a @@ -1865,6 +2044,15 @@ panel: I - room: Elements Area panel: A + panel_doors: + Giant Sevens: + item_name: Giant Seven Panels + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A paintings: - id: crown_painting orientation: east @@ -1972,14 +2160,31 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + panel_doors: + Access: + item_name: Orange Tower Panels + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST progression: Progressive Orange Tower: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor - - Sixth Floor - - Seventh Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor Orange Tower First Floor: entrances: Hub Room: @@ -2022,6 +2227,10 @@ - SALT - room: Directional Gallery panel: PEPPER + panel_doors: + SECRET: + panels: + - SECRET sunwarps: - dots: 4 direction: enter @@ -2174,6 +2383,10 @@ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts panels: - HOT CRUSTS + panel_doors: + HOT CRUSTS: + panels: + - HOT CRUSTS sunwarps: - dots: 5 direction: enter @@ -2288,6 +2501,12 @@ panels: - SIZE (Small) - SIZE (Big) + panel_doors: + SIZE: + item_name: Orange Tower Fifth Floor - SIZE Panels + panels: + - SIZE (Small) + - SIZE (Big) paintings: - id: hi_solved_painting3 orientation: south @@ -2631,6 +2850,15 @@ - SECOND - THIRD - FOURTH + panel_doors: + FIRST SECOND THIRD FOURTH: + item_name: Courtyard - Ordinal Panels + panel_group: Backside Entrance Panels + panels: + - FIRST + - SECOND + - THIRD + - FOURTH The Colorful (White): entrances: Courtyard: True @@ -2648,6 +2876,12 @@ location_name: The Colorful - White panels: - BEGIN + panel_doors: + BEGIN: + item_name: The Colorful - BEGIN (Panel) + panel_group: Colorful Panels + panels: + - BEGIN The Colorful (Black): entrances: The Colorful (White): @@ -2668,6 +2902,12 @@ door_group: Colorful Doors panels: - FOUND + panel_doors: + FOUND: + item_name: The Colorful - FOUND (Panel) + panel_group: Colorful Panels + panels: + - FOUND The Colorful (Red): entrances: The Colorful (Black): @@ -2688,6 +2928,12 @@ door_group: Colorful Doors panels: - LOAF + panel_doors: + LOAF: + item_name: The Colorful - LOAF (Panel) + panel_group: Colorful Panels + panels: + - LOAF The Colorful (Yellow): entrances: The Colorful (Red): @@ -2708,6 +2954,12 @@ door_group: Colorful Doors panels: - CREAM + panel_doors: + CREAM: + item_name: The Colorful - CREAM (Panel) + panel_group: Colorful Panels + panels: + - CREAM The Colorful (Blue): entrances: The Colorful (Yellow): @@ -2728,6 +2980,12 @@ door_group: Colorful Doors panels: - SUN + panel_doors: + SUN: + item_name: The Colorful - SUN (Panel) + panel_group: Colorful Panels + panels: + - SUN The Colorful (Purple): entrances: The Colorful (Blue): @@ -2748,6 +3006,12 @@ door_group: Colorful Doors panels: - SPOON + panel_doors: + SPOON: + item_name: The Colorful - SPOON (Panel) + panel_group: Colorful Panels + panels: + - SPOON The Colorful (Orange): entrances: The Colorful (Purple): @@ -2768,6 +3032,12 @@ door_group: Colorful Doors panels: - LETTERS + panel_doors: + LETTERS: + item_name: The Colorful - LETTERS (Panel) + panel_group: Colorful Panels + panels: + - LETTERS The Colorful (Green): entrances: The Colorful (Orange): @@ -2788,6 +3058,12 @@ door_group: Colorful Doors panels: - WALLS + panel_doors: + WALLS: + item_name: The Colorful - WALLS (Panel) + panel_group: Colorful Panels + panels: + - WALLS The Colorful (Brown): entrances: The Colorful (Green): @@ -2808,6 +3084,12 @@ door_group: Colorful Doors panels: - IRON + panel_doors: + IRON: + item_name: The Colorful - IRON (Panel) + panel_group: Colorful Panels + panels: + - IRON The Colorful (Gray): entrances: The Colorful (Brown): @@ -2828,6 +3110,12 @@ door_group: Colorful Doors panels: - OBSTACLE + panel_doors: + OBSTACLE: + item_name: The Colorful - OBSTACLE (Panel) + panel_group: Colorful Panels + panels: + - OBSTACLE The Colorful: entrances: The Colorful (Gray): @@ -2866,26 +3154,48 @@ orientation: north progression: Progressive Colorful: - - room: The Colorful (White) - door: Progress Door - - room: The Colorful (Black) - door: Progress Door - - room: The Colorful (Red) - door: Progress Door - - room: The Colorful (Yellow) - door: Progress Door - - room: The Colorful (Blue) - door: Progress Door - - room: The Colorful (Purple) - door: Progress Door - - room: The Colorful (Orange) - door: Progress Door - - room: The Colorful (Green) - door: Progress Door - - room: The Colorful (Brown) - door: Progress Door - - room: The Colorful (Gray) - door: Progress Door + doors: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + panel_doors: + - room: The Colorful (White) + panel_door: BEGIN + - room: The Colorful (Black) + panel_door: FOUND + - room: The Colorful (Red) + panel_door: LOAF + - room: The Colorful (Yellow) + panel_door: CREAM + - room: The Colorful (Blue) + panel_door: SUN + - room: The Colorful (Purple) + panel_door: SPOON + - room: The Colorful (Orange) + panel_door: LETTERS + - room: The Colorful (Green) + panel_door: WALLS + - room: The Colorful (Brown) + panel_door: IRON + - room: The Colorful (Gray) + panel_door: OBSTACLE Welcome Back Area: entrances: Starting Room: @@ -2958,6 +3268,10 @@ door_group: Hedge Maze Doors panels: - STRAYS + panel_doors: + STRAYS: + panels: + - STRAYS paintings: - id: arrows_painting_8 orientation: south @@ -3155,6 +3469,13 @@ panel: I - room: Elements Area panel: A + panel_doors: + UNCOVER: + panels: + - UNCOVER + OXEN: + panels: + - OXEN paintings: - id: clock_painting_5 orientation: east @@ -3524,6 +3845,13 @@ - RISE (Sunrise) - ZEN - SON + panel_doors: + UNOPEN: + panels: + - UNOPEN + BEGIN: + panels: + - BEGIN paintings: - id: pencil_painting2 orientation: west @@ -3819,6 +4147,34 @@ item_group: Achievement Room Entrances panels: - ZERO + panel_doors: + ZERO: + panels: + - ZERO + PEN: + panels: + - PEN + TWO: + item_name: Two Panels + panels: + - TWO (1) + - TWO (2) + THREE: + item_name: Three Panels + panels: + - THREE (1) + - THREE (2) + - THREE (3) + FOUR: + item_name: Four Panels + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR paintings: - id: maze_painting_3 enter_only: True @@ -3994,6 +4350,10 @@ panel: FIVE (1) - room: Directional Gallery panel: FIVE (2) + First Six: + event: True + panels: + - SIX Sevens: id: - Count Up Room Area Doors/Door_seven_hider @@ -4102,12 +4462,109 @@ panel: NINE - room: Elements Area panel: NINE + panel_doors: + FIVE: + item_name: Five Panels + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + SIX: + item_name: Six Panels + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + SEVEN: + item_name: Seven Panels + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + EIGHT: + item_name: Eight Panels + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + NINE: + item_name: Nine Panels + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE paintings: - id: smile_painting_5 enter_only: True orientation: east required_door: door: Eights + progression: + Progressive Number Hunt: + panel_doors: + - room: Outside The Undeterred + panel_door: TWO + - room: Outside The Undeterred + panel_door: THREE + - room: Outside The Undeterred + panel_door: FOUR + - FIVE + - SIX + - SEVEN + - EIGHT + - NINE + - room: Outside The Undeterred + panel_door: ZERO Directional Gallery: entrances: Outside The Agreeable: @@ -4195,7 +4652,7 @@ tag: midorange required_door: room: Number Hunt - door: Sixes + door: First Six PARANOID: id: Backside Room/Panel_paranoid_paranoid tag: midwhite @@ -4203,7 +4660,7 @@ exclude_reduce: True required_door: room: Number Hunt - door: Sixes + door: First Six YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite @@ -4266,6 +4723,11 @@ panels: - room: Color Hunt panel: YELLOW + panel_doors: + TURN LEARN: + panels: + - TURN + - LEARN paintings: - id: smile_painting_7 orientation: south @@ -4277,7 +4739,7 @@ move: True required_door: room: Number Hunt - door: Sixes + door: First Six - id: boxes_painting orientation: south - id: cherry_painting @@ -4344,6 +4806,34 @@ id: Rock Room Doors/Door_hint panels: - EXIT + panel_doors: + EXIT: + panels: + - EXIT + RED: + panel_group: Color Hunt Panels + panels: + - RED + BLUE: + panel_group: Color Hunt Panels + panels: + - BLUE + YELLOW: + panel_group: Color Hunt Panels + panels: + - YELLOW + ORANGE: + panel_group: Color Hunt Panels + panels: + - ORANGE + PURPLE: + panel_group: Color Hunt Panels + panels: + - PURPLE + GREEN: + panel_group: Color Hunt Panels + panels: + - GREEN paintings: - id: arrows_painting_7 orientation: east @@ -4481,6 +4971,14 @@ event: True panels: - HEART + panel_doors: + FARTHER: + panel_group: Backside Entrance Panels + panels: + - FARTHER + MIDDLE: + panels: + - MIDDLE The Bearer (East): entrances: Cross Tower (East): True @@ -5333,6 +5831,11 @@ item_name: Knight Night Room - Exit panels: - TRUSTED + panel_doors: + TRUSTED: + item_name: Knight Night Room - TRUSTED (Panel) + panels: + - TRUSTED Knight Night Exit: entrances: Knight Night (Outer Ring): @@ -6017,6 +6520,10 @@ item_group: Achievement Room Entrances panels: - SHRINK + panel_doors: + SHRINK: + panels: + - SHRINK The Wondrous (Doorknob): entrances: Outside The Wondrous: @@ -6228,18 +6735,36 @@ - KEEP - BAILEY - TOWER + panel_doors: + CASTLE: + item_name: Hallway Room - First Room Panels + panel_group: Hallway Room Panels + panels: + - WALL + - KEEP + - BAILEY + - TOWER paintings: - id: panda_painting orientation: south progression: Progressive Hallway Room: - - Exit - - room: Hallway Room (2) - door: Exit - - room: Hallway Room (3) - door: Exit - - room: Hallway Room (4) - door: Exit + doors: + - Exit + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + panel_doors: + - CASTLE + - room: Hallway Room (2) + panel_door: COUNTERCLOCKWISE + - room: Hallway Room (3) + panel_door: TRANSFORMATION + - room: Hallway Room (4) + panel_door: WHEELBARROW Hallway Room (2): entrances: Hallway Room (1): @@ -6278,6 +6803,15 @@ - CLOCK - ER - COUNT + panel_doors: + COUNTERCLOCKWISE: + item_name: Hallway Room - Second Room Panels + panel_group: Hallway Room Panels + panels: + - WISE + - CLOCK + - ER + - COUNT Hallway Room (3): entrances: Hallway Room (2): @@ -6316,6 +6850,15 @@ - FORM - A - SHUN + panel_doors: + TRANSFORMATION: + item_name: Hallway Room - Third Room Panels + panel_group: Hallway Room Panels + panels: + - TRANCE + - FORM + - A + - SHUN Hallway Room (4): entrances: Hallway Room (3): @@ -6338,6 +6881,12 @@ panels: - WHEEL include_reduce: True + panel_doors: + WHEELBARROW: + item_name: Hallway Room - WHEEL + panel_group: Hallway Room Panels + panels: + - WHEEL Elements Area: entrances: Roof: True @@ -6412,6 +6961,10 @@ panels: - room: The Wanderer panel: Achievement + panel_doors: + WANDERLUST: + panels: + - WANDERLUST The Wanderer: entrances: Outside The Wanderer: @@ -6553,6 +7106,10 @@ item_group: Achievement Room Entrances panels: - ORDER + panel_doors: + ORDER: + panels: + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6566,10 +7123,11 @@ orientation: south progression: Progressive Art Gallery: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor Art Gallery (Second Floor): entrances: Art Gallery: @@ -7281,8 +7839,8 @@ id: Panel Room/Panel_broomed_bedroom colors: yellow tag: midyellow - required_door: - door: Excavation + required_panel: + panel: WALL (1) LAYS: id: Panel Room/Panel_lays_maze colors: purple @@ -7309,13 +7867,24 @@ Excavation: event: True panels: - - WALL (1) + - STAIRS Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 panels: - BASE + panel_doors: + STAIRS: + panel_group: Room Room Panels + panels: + - STAIRS + Colors: + panel_group: Room Room Panels + panels: + - BROOMED + - LAYS + - BASE Cellar: entrances: Room Room: @@ -7354,6 +7923,11 @@ panels: - KITTEN - CAT + panel_doors: + KITTEN CAT: + panels: + - KITTEN + - CAT paintings: - id: arrows_painting_2 orientation: east @@ -7608,6 +8182,10 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + OPEN: + panels: + - OPEN The Scientific: entrances: Outside The Scientific: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 4a751b25ec5f143b6b055fd2043a6543754ef5b1..d221b8168d9164576b3f1c5b2fc57c604da5f100 100644 GIT binary patch delta 33771 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 WLAMjq6B6>Y{A>5)@ndWwlKvm@gojE1 From ab0903679c6de5407de01a4ba139b495be013ba5 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Jul 2024 11:57:10 -0700 Subject: [PATCH 101/393] Factorio: Fix ap-get-technology nil value crashes (#3517) --- worlds/factorio/data/mod_template/control.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a5f67..ace231e12b4b 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) From e764da3dc6c476be7bd69267eebb8ad511bc3fbb Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 28 Jul 2024 16:27:39 -0500 Subject: [PATCH 102/393] HK: Options API updates, et al. (#3428) * updates HK to consistently use world.random, use world.options, don't use world = self.multiworld, and remove some things from the logicMixin * Update HK to new options dataclass * Move completion condition helpers to Rules.py * updates from review --- worlds/hk/Options.py | 6 +- worlds/hk/Rules.py | 39 ++++++++++ worlds/hk/__init__.py | 162 +++++++++++++++++------------------------- 3 files changed, 108 insertions(+), 99 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 38be2cd794a1..e2602036a24e 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,10 +1,12 @@ import typing import re +from dataclasses import dataclass, make_dataclass + from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from schema import And, Schema, Optional -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -538,3 +540,5 @@ class CostSanityHybridChance(Range): }, **cost_sanity_weights } + +HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,)) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index a3c7e13cf02b..e162e1dfa81c 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -49,3 +49,42 @@ def set_rules(hk_world: World): if term == "GEO": # No geo logic! continue add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + + +def _hk_nail_combat(state, player) -> bool: + return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) + + +def _hk_can_beat_thk(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and _hk_nail_combat(state, player) + and ( + state.has_any({'LEFTDASH', 'RIGHTDASH'}, player) + or state._hk_option(player, 'ProficientCombat') + ) + and state.has('FOCUS', player) + ) + + +def _hk_siblings_ending(state, player) -> bool: + return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3) + + +def _hk_can_beat_radiance(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and _hk_nail_combat(state, player) + and state.has('WHITEFRAGMENT', player, 3) + and state.has('DREAMNAIL', player) + and ( + (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player)) + or state.has('WINGS', player) + ) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and ( + (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks + or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive + ) + ) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index fbc6461f6aab..e5065876ddf3 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -10,9 +10,9 @@ from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions -from .Rules import set_rules, cost_terms +from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option + shop_to_option, HKOptions from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names @@ -142,7 +142,8 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - option_definitions = hollow_knight_options + options_dataclass = HKOptions + options: HKOptions web = HKWeb() @@ -155,8 +156,8 @@ class HKWorld(World): charm_costs: typing.List[int] cached_filler_items = {} - def __init__(self, world, player): - super(HKWorld, self).__init__(world, player) + def __init__(self, multiworld, player): + super(HKWorld, self).__init__(multiworld, player) self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { location: list() for location in multi_locations } @@ -165,29 +166,29 @@ def __init__(self, world, player): self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) def generate_early(self): - world = self.multiworld - charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) - self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) - # world.exclude_locations[self.player].value.update(white_palace_locations) + options = self.options + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # options.exclude_locations.value.update(white_palace_locations) for term, data in cost_terms.items(): - mini = getattr(world, f"Minimum{data.option}Price")[self.player] - maxi = getattr(world, f"Maximum{data.option}Price")[self.player] + mini = getattr(options, f"Minimum{data.option}Price") + maxi = getattr(options, f"Maximum{data.option}Price") # if minimum > maximum, set minimum to maximum mini.value = min(mini.value, maxi.value) self.ranges[term] = mini.value, maxi.value - world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], + self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], True, None, "Event", self.player)) def white_palace_exclusions(self): exclusions = set() - wp = self.multiworld.WhitePalace[self.player] + wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: exclusions.add("King_Fragment") - if self.multiworld.RandomizeCharms[self.player]: + if self.options.RandomizeCharms: # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) @@ -200,7 +201,7 @@ def create_regions(self): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -230,12 +231,12 @@ def create_items(self): pool: typing.List[HKItem] = [] wp_exclusions = self.white_palace_exclusions() junk_replace: typing.Set[str] = set() - if self.multiworld.RemoveSpellUpgrades[self.player]: + if self.options.RemoveSpellUpgrades: junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) randomized_starting_items = set() for attr, items in randomizable_starting_items.items(): - if getattr(self.multiworld, attr)[self.player]: + if getattr(self.options, attr): randomized_starting_items.update(items) # noinspection PyShadowingNames @@ -257,7 +258,7 @@ def _add(item_name: str, location_name: str, randomized: bool): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) + item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) if location_name == "Start": if item_name in randomized_starting_items: @@ -281,55 +282,55 @@ def _add(item_name: str, location_name: str, randomized: bool): location.progress_type = LocationProgressType.EXCLUDED for option_key, option in hollow_knight_randomize_options.items(): - randomized = getattr(self.multiworld, option_key)[self.player] - if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): + randomized = getattr(self.options, option_key) + if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]): continue for item_name, location_name in zip(option.items, option.locations): if item_name in junk_replace: item_name = self.get_filler_item_name() - if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ - (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): + if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \ + (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak): _add("Left_" + item_name, location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized) continue - if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: + if item_name == "Mantis_Claw" and self.options.SplitMantisClaw: _add("Left_" + item_name, "Left_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized) continue - if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: - if self.multiworld.random.randint(0, 1): + if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak: + if self.random.randint(0, 1): item_name = "Left_Mothwing_Cloak" else: item_name = "Right_Mothwing_Cloak" - if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: + if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms: _add("Grimmchild1", location_name, randomized) continue _add(item_name, location_name, randomized) - if self.multiworld.RandomizeElevatorPass[self.player]: + if self.options.RandomizeElevatorPass: randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): + for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): loc = self.create_location(shop) unfilled_locations += 1 # Balance the pool item_count = len(pool) - additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) + additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value) # Add additional shop items, as needed. if additional_shop_items > 0: shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) - if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there + if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') if shops: for _ in range(additional_shop_items): - shop = self.multiworld.random.choice(shops) + shop = self.random.choice(shops) loc = self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: @@ -355,7 +356,7 @@ def sort_shops_by_cost(self): loc.costs = costs def apply_costsanity(self): - setting = self.multiworld.CostSanity[self.player].value + setting = self.options.CostSanity.value if not setting: return # noop @@ -369,10 +370,10 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: return {k: v for k, v in weights.items() if v} - random = self.multiworld.random - hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value + random = self.random + hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value weights = { - data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value + data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() } weights_geoless = dict(weights) @@ -427,22 +428,22 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: location.sort_costs() def set_rules(self): - world = self.multiworld + multiworld = self.multiworld player = self.player - goal = world.Goal[player] + goal = self.options.Goal if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player) elif goal == Goal.option_godhome: - world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) + multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: - world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) else: # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) set_rules(self) @@ -450,8 +451,8 @@ def fill_slot_data(self): slot_data = {} options = slot_data["options"] = {} - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] + for option_name in hollow_knight_options: + option = getattr(self.options, option_name) try: optionvalue = int(option.value) except TypeError: @@ -460,10 +461,10 @@ def fill_slot_data(self): options[option_name] = optionvalue # 32 bit int - slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) + slot_data["seed"] = self.random.randint(-2147483647, 2147483646) # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.multiworld.CostSanity[self.player]: + if not self.options.CostSanity: for shop, terms in shop_cost_types.items(): unit = cost_terms[next(iter(terms))].option if unit == "Geo": @@ -498,7 +499,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation: basename = name if name in shop_cost_types: costs = { - term: self.multiworld.random.randint(*self.ranges[term]) + term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[name] } elif name in vanilla_location_costs: @@ -512,7 +513,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation: region = self.multiworld.get_region("Menu", self.player) - if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: + if vanilla and not self.options.AddUnshuffledLocations: loc = HKLocation(self.player, name, None, region, costs=costs, vanilla=vanilla, basename=basename) @@ -560,26 +561,26 @@ def remove(self, state, item: HKItem) -> bool: return change @classmethod - def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): - hk_players = world.get_game_players(cls.game) + def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): + hk_players = multiworld.get_game_players(cls.game) spoiler_handle.write('\n\nCharm Notches:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] for charm_number, cost in enumerate(hk_world.charm_costs): spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write('\n\nShop Prices:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] - if world.CostSanity[player].value: + if hk_world.options.CostSanity: for loc in sorted( ( - loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) + loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player))) if loc.costs ), key=operator.attrgetter('name') ): @@ -603,15 +604,15 @@ def get_filler_item_name(self) -> str: 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeRancidEggs' ): - if getattr(self.multiworld, group): + if getattr(self.options, group): fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in exclusions) self.cached_filler_items[self.player] = fillers - return self.multiworld.random.choice(self.cached_filler_items[self.player]) + return self.random.choice(self.cached_filler_items[self.player]) -def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: - ret = Region(name, player, world) +def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region: + ret = Region(name, player, multiworld) if location_names: for location in location_names: loc_id = HKWorld.location_name_to_id.get(location, None) @@ -684,42 +685,7 @@ def _hk_notches(self, player: int, *notches: int) -> int: return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) def _hk_option(self, player: int, option_name: str) -> int: - return getattr(self.multiworld, option_name)[player].value + return getattr(self.multiworld.worlds[player].options, option_name).value def _hk_start(self, player, start_location: str) -> bool: - return self.multiworld.StartLocation[player] == start_location - - def _hk_nail_combat(self, player: int) -> bool: - return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) - - def _hk_can_beat_thk(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and self._hk_nail_combat(player) - and ( - self.has_any({'LEFTDASH', 'RIGHTDASH'}, player) - or self._hk_option(player, 'ProficientCombat') - ) - and self.has('FOCUS', player) - ) - - def _hk_siblings_ending(self, player: int) -> bool: - return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3) - - def _hk_can_beat_radiance(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and self._hk_nail_combat(player) - and self.has('WHITEFRAGMENT', player, 3) - and self.has('DREAMNAIL', player) - and ( - (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player)) - or self.has('WINGS', player) - ) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and ( - (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks - or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive - ) - ) + return self.multiworld.worlds[player].options.StartLocation == start_location From fac72dbc207f698f8c61f37953189f1f8a55de66 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:40:58 -0400 Subject: [PATCH 103/393] FFMQ: Fix reset protection (#3710) * Revert reset protection * Fix reset protection --------- Co-authored-by: alchav --- worlds/ffmq/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 6cb35dd3b4be..93688a6116f6 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ async def game_watcher(self, ctx): received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 != b'01' or check_2 != b'01': + if check_1 != b'\x01' or check_2 != b'\x01': return def get_range(data_range): From 80daa092a7782c941411cd1568f232e7781cc316 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 29 Jul 2024 13:42:16 -0400 Subject: [PATCH 104/393] - Take shipsanity moss out of shipsanity crops (#3709) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 0d7a10f95496..608b6a5f576a 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2212,7 +2212,7 @@ id,region,name,tags,mod_name 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", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", 3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", 3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", From 954d72800541c8bc6e515f9e046d50a203ac9ab5 Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:09:51 -0700 Subject: [PATCH 105/393] sc2: Removing unused dependency in requirements.txt (#3697) * sc2: Removing unused dependency in requirements.txt * sc2: Add missing newline in requirements.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/sc2/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/sc2/requirements.txt b/worlds/sc2/requirements.txt index 9b84863c4590..5bc808b639db 100644 --- a/worlds/sc2/requirements.txt +++ b/worlds/sc2/requirements.txt @@ -1,2 +1 @@ nest-asyncio >= 1.5.5 -six >= 1.16.0 \ No newline at end of file From 77e3f9fbefa32a51c6c6ce1c6a0aee584c0a72f1 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Mon, 29 Jul 2024 20:13:44 -0400 Subject: [PATCH 106/393] WebHost: Fix NamedRange values clamping to the range (#3613) If a NamedRange has a `special_range_names` entry outside the `range_start` and `range_end`, the HTML5 range input will clamp the submitted value to the closest value in the range. These means that, for example, Pokemon RB's "HM Compatibility" option's "Vanilla (-1)" option would instead get posted as "0" rather than "-1". This change updates NamedRange to behave like TextChoice, where the select element has a `name` attribute matching the option, and there is an additional element to be able to provide an option other than the select element's choices. This uses a different suffix of `-range` rather than `-custom` that TextChoice uses. The reason is we need some way to decide whether to use the custom value or the select value, and that method needs to work without JavaScript. For TextChoice this is easy, if the custom field is empty use the select element. For NamedRange this is more difficult as the browser will always submit *something*. My choice was to only use the value from the range if the select box is set to "custom". Since this only happens with JS as "custom' is hidden, I made the range hidden under no-JS. If it's preferred, I could make the select box hidden instead. Let me know. This PR also makes the `js-required` class set `display: none` with `!important` as otherwise the class wouldn't work on any rule that had `display: flex` with more specificity than a single class. --- WebHostLib/options.py | 7 +++++++ WebHostLib/templates/playerOptions/macros.html | 8 ++++---- WebHostLib/templates/playerOptions/playerOptions.html | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 33339daa1983..15b7bd61ceee 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -231,6 +231,13 @@ def generate_yaml(game: str): del options[key] + # Detect keys which end with -range, indicating a NamedRange with a possible custom value + elif key_parts[-1].endswith("-range"): + if options[key_parts[-1][:-6]] == "custom": + options[key_parts[-1][:-6]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 415739b861a1..30a4fc78dff3 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -54,7 +54,7 @@ {% macro NamedRange(option_name, option) %} {{ OptionTitle(option_name, option) }}
- {% for key, val in option.special_range_names.items() %} {% if option.default == val %} @@ -64,17 +64,17 @@ {% endfor %} -
+
- + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} {{ RandomizeButton(option_name, option) }} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index aeb6e864a54b..73de5d56eb20 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -11,7 +11,7 @@ From 1d19da0c76cca65b1b5b6916345486726b96e188 Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 31 Jul 2024 11:50:04 +0200 Subject: [PATCH 107/393] Timespinner: migrate to new options api and correct random (#2485) * Implemented new options system into Timespinner * Fixed typo * Fixed typo * Fixed slotdata maybe * Fixes * more fixes * Fixed failing unit tests * Implemented options backwards comnpatibility * Fixed option fallbacks * Implemented review results * Fixed logic bug * Fixed python 3.8/3.9 compatibility * Replaced one more multiworld option usage * Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/timespinner/Locations.py | 21 +- worlds/timespinner/LogicExtensions.py | 23 +- worlds/timespinner/Options.py | 343 +++++++++++++++------ worlds/timespinner/PreCalculatedWeights.py | 68 ++-- worlds/timespinner/Regions.py | 24 +- worlds/timespinner/__init__.py | 185 ++++++----- 6 files changed, 424 insertions(+), 240 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 7b378b4637fa..86839f0f2167 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,6 +1,6 @@ from typing import List, Optional, Callable, NamedTuple -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic @@ -14,11 +14,10 @@ class LocationData(NamedTuple): rule: Optional[Callable[[CollectionState], bool]] = None -def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> List[LocationData]: - - flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) +def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]: + flooded: Optional[PreCalculatedWeights] = precalculated_weights + logic = TimespinnerLogic(player, options, precalculated_weights) # 1337000 - 1337155 Generic locations # 1337171 - 1337175 New Pickup checks @@ -203,7 +202,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ] # 1337156 - 1337170 Downloads - if not world or is_option_enabled(world, player, "DownloadableItems"): + if not options or options.downloadable_items: location_table += ( LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), @@ -223,13 +222,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ) # 1337176 - 1337176 Cantoran - if not world or is_option_enabled(world, player, "Cantoran"): + if not options or options.cantoran: location_table += ( LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176), ) # 1337177 - 1337198 Lore Checks - if not world or is_option_enabled(world, player, "LoreChecks"): + if not options or options.lore_checks: location_table += ( LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177), LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178), @@ -258,7 +257,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], # 1337199 - 1337236 Reserved for future use # 1337237 - 1337245 GyreArchives - if not world or is_option_enabled(world, player, "GyreArchives"): + if not options or options.gyre_archives: location_table += ( LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237), LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238), diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index d316a936b02f..6c9cb3f684a0 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -1,6 +1,6 @@ -from typing import Union -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from typing import Union, Optional +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights @@ -10,17 +10,18 @@ class TimespinnerLogic: flag_unchained_keys: bool flag_eye_spy: bool flag_specific_keycards: bool - pyramid_keys_unlock: Union[str, None] - present_keys_unlock: Union[str, None] - past_keys_unlock: Union[str, None] - time_keys_unlock: Union[str, None] + pyramid_keys_unlock: Optional[str] + present_keys_unlock: Optional[str] + past_keys_unlock: Optional[str] + time_keys_unlock: Optional[str] - def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + def __init__(self, player: int, options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards") - self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy") - self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys") + self.flag_specific_keycards = bool(options and options.specific_keycards) + self.flag_eye_spy = bool(options and options.eye_spy) + self.flag_unchained_keys = bool(options and options.unchained_keys) if precalculated_weights: if self.flag_unchained_keys: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index f7921fcb81e0..20ad8132c45f 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,59 +1,50 @@ -from typing import Dict, Union, List -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList +from dataclasses import dataclass +from typing import Type, Any +from typing import Dict +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option +from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions from schema import Schema, And, Optional, Or - class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" display_name = "Start with Jewelry Box" - class DownloadableItems(DefaultOnToggle): "With the tablet you will be able to download items at terminals" display_name = "Downloadable items" - class EyeSpy(Toggle): "Requires Oculus Ring in inventory to be able to break hidden walls." display_name = "Eye Spy" - class StartWithMeyef(Toggle): "Start with Meyef, ideal for when you want to play multiplayer." display_name = "Start with Meyef" - class QuickSeed(Toggle): "Start with Talaria Attachment, Nyoom!" display_name = "Quick seed" - class SpecificKeycards(Toggle): "Keycards can only open corresponding doors" display_name = "Specific Keycards" - class Inverted(Toggle): "Start in the past" display_name = "Inverted" - class GyreArchives(Toggle): "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" display_name = "Gyre Archives" - class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" - class LoreChecks(Toggle): "Memories and journal entries contain items." display_name = "Lore Checks" - class BossRando(Choice): "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" @@ -62,7 +53,6 @@ class BossRando(Choice): option_unscaled = 2 alias_true = 1 - class EnemyRando(Choice): "Wheter enemies will be randomized, and if their damage/hp should be scaled." display_name = "Enemy Randomization" @@ -72,7 +62,6 @@ class EnemyRando(Choice): option_ryshia = 3 alias_true = 1 - class DamageRando(Choice): "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." display_name = "Damage Rando" @@ -85,7 +74,6 @@ class DamageRando(Choice): option_manual = 6 alias_true = 2 - class DamageRandoOverrides(OptionDict): """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds""" @@ -191,7 +179,6 @@ class DamageRandoOverrides(OptionDict): "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, } - class HpCap(Range): "Sets the number that Lunais's HP maxes out at." display_name = "HP Cap" @@ -199,7 +186,6 @@ class HpCap(Range): range_end = 999 default = 999 - class LevelCap(Range): """Sets the max level Lunais can achieve.""" display_name = "Level Cap" @@ -207,20 +193,17 @@ class LevelCap(Range): range_end = 99 default = 99 - class ExtraEarringsXP(Range): """Adds additional XP granted by Galaxy Earrings.""" display_name = "Extra Earrings XP" range_start = 0 range_end = 24 default = 0 - class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" - class ShopFill(Choice): """Sets the items for sale in Merchant Crow's shops. Default: No sunglasses or trendy jacket, but sand vials for sale. @@ -233,12 +216,10 @@ class ShopFill(Choice): option_vanilla = 2 option_empty = 3 - class ShopWarpShards(DefaultOnToggle): "Shops always sell warp shards (when keys possessed), ignoring inventory setting." display_name = "Always Sell Warp Shards" - class ShopMultiplier(Range): "Multiplier for the cost of items in the shop. Set to 0 for free shops." display_name = "Shop Price Multiplier" @@ -246,7 +227,6 @@ class ShopMultiplier(Range): range_end = 10 default = 1 - class LootPool(Choice): """Sets the items that drop from enemies (does not apply to boss reward checks) Vanilla: Drops are the same as the base game @@ -257,7 +237,6 @@ class LootPool(Choice): option_randomized = 1 option_empty = 2 - class DropRateCategory(Choice): """Sets the drop rate when 'Loot Pool' is set to 'Random' Tiered: Based on item rarity/value @@ -271,7 +250,6 @@ class DropRateCategory(Choice): option_randomized = 2 option_fixed = 3 - class FixedDropRate(Range): "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" display_name = "Fixed Drop Rate" @@ -279,7 +257,6 @@ class FixedDropRate(Range): range_end = 100 default = 5 - class LootTierDistro(Choice): """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items @@ -291,32 +268,26 @@ class LootTierDistro(Choice): option_full_random = 1 option_inverted_weight = 2 - class ShowBestiary(Toggle): "All entries in the bestiary are visible, without needing to kill one of a given enemy first" display_name = "Show Bestiary Entries" - class ShowDrops(Toggle): "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" display_name = "Show Bestiary Item Drops" - class EnterSandman(Toggle): "The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces" display_name = "Enter Sandman" - class DadPercent(Toggle): """The win condition is beating the boss of Emperor's Tower""" display_name = "Dad Percent" - class RisingTides(Toggle): """Random areas are flooded or drained, can be further specified with RisingTidesOverrides""" display_name = "Rising Tides" - def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]: if with_save_point_option: return { @@ -341,7 +312,6 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D "Flooded") } - class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" @@ -373,13 +343,11 @@ class RisingTidesOverrides(OptionDict): "Lab": { "Dry": 67, "Flooded": 33 }, } - class UnchainedKeys(Toggle): """Start with Twin Pyramid Key, which does not give free warp; warp items for Past, Present, (and ??? with Enter Sandman) can be found.""" display_name = "Unchained Keys" - class TrapChance(Range): """Chance of traps in the item pool. Traps will only replace filler items such as potions, vials and antidotes""" @@ -388,67 +356,256 @@ class TrapChance(Range): range_end = 100 default = 10 - class Traps(OptionList): """List of traps that may be in the item pool to find""" display_name = "Traps Types" valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] - class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" - display_name = "Past Wheel & Spindle Warp" - - -# Some options that are available in the timespinner randomizer arent currently implemented -timespinner_options: Dict[str, Option] = { - "StartWithJewelryBox": StartWithJewelryBox, - "DownloadableItems": DownloadableItems, - "EyeSpy": EyeSpy, - "StartWithMeyef": StartWithMeyef, - "QuickSeed": QuickSeed, - "SpecificKeycards": SpecificKeycards, - "Inverted": Inverted, - "GyreArchives": GyreArchives, - "Cantoran": Cantoran, - "LoreChecks": LoreChecks, - "BossRando": BossRando, - "EnemyRando": EnemyRando, - "DamageRando": DamageRando, - "DamageRandoOverrides": DamageRandoOverrides, - "HpCap": HpCap, - "LevelCap": LevelCap, - "ExtraEarringsXP": ExtraEarringsXP, - "BossHealing": BossHealing, - "ShopFill": ShopFill, - "ShopWarpShards": ShopWarpShards, - "ShopMultiplier": ShopMultiplier, - "LootPool": LootPool, - "DropRateCategory": DropRateCategory, - "FixedDropRate": FixedDropRate, - "LootTierDistro": LootTierDistro, - "ShowBestiary": ShowBestiary, - "ShowDrops": ShowDrops, - "EnterSandman": EnterSandman, - "DadPercent": DadPercent, - "RisingTides": RisingTides, - "RisingTidesOverrides": RisingTidesOverrides, - "UnchainedKeys": UnchainedKeys, - "TrapChance": TrapChance, - "Traps": Traps, - "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, - "DeathLink": DeathLink, -} - - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: - option = getattr(world, name, None) - if option == None: - return 0 - - return option[player].value + display_name = "Back to the future" + +@dataclass +class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): + start_with_jewelry_box: StartWithJewelryBox + downloadable_items: DownloadableItems + eye_spy: EyeSpy + start_with_meyef: StartWithMeyef + quick_seed: QuickSeed + specific_keycards: SpecificKeycards + inverted: Inverted + gyre_archives: GyreArchives + cantoran: Cantoran + lore_checks: LoreChecks + boss_rando: BossRando + damage_rando: DamageRando + damage_rando_overrides: DamageRandoOverrides + hp_cap: HpCap + level_cap: LevelCap + extra_earrings_xp: ExtraEarringsXP + boss_healing: BossHealing + shop_fill: ShopFill + shop_warp_shards: ShopWarpShards + shop_multiplier: ShopMultiplier + loot_pool: LootPool + drop_rate_category: DropRateCategory + fixed_drop_rate: FixedDropRate + loot_tier_distro: LootTierDistro + show_bestiary: ShowBestiary + show_drops: ShowDrops + enter_sandman: EnterSandman + dad_percent: DadPercent + rising_tides: RisingTides + rising_tides_overrides: RisingTidesOverrides + unchained_keys: UnchainedKeys + back_to_the_future: PresentAccessWithWheelAndSpindle + trap_chance: TrapChance + traps: Traps + +class HiddenDamageRandoOverrides(DamageRandoOverrides): + """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that + you don't specify will roll with 1/1/1 as odds""" + visibility = Visibility.none + +class HiddenRisingTidesOverrides(RisingTidesOverrides): + """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. + Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + visibility = Visibility.none + +class HiddenTraps(Traps): + """List of traps that may be in the item pool to find""" + visibility = Visibility.none + +class OptionsHider: + @classmethod + def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ + return new_option + +class HasReplacedCamelCase(Toggle): + """For internal use will display a warning message if true""" + visibility = Visibility.none + +@dataclass +class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): + StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore + EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore + StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore + QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore + SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore + Inverted: OptionsHider.hidden(Inverted) # type: ignore + GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore + Cantoran: OptionsHider.hidden(Cantoran) # type: ignore + LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore + BossRando: OptionsHider.hidden(BossRando) # type: ignore + DamageRando: OptionsHider.hidden(DamageRando) # type: ignore + DamageRandoOverrides: HiddenDamageRandoOverrides + HpCap: OptionsHider.hidden(HpCap) # type: ignore + LevelCap: OptionsHider.hidden(LevelCap) # type: ignore + ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore + BossHealing: OptionsHider.hidden(BossHealing) # type: ignore + ShopFill: OptionsHider.hidden(ShopFill) # type: ignore + ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore + ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore + LootPool: OptionsHider.hidden(LootPool) # type: ignore + DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore + FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore + LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore + ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore + ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore + EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore + DadPercent: OptionsHider.hidden(DadPercent) # type: ignore + RisingTides: OptionsHider.hidden(RisingTides) # type: ignore + RisingTidesOverrides: HiddenRisingTidesOverrides + UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: OptionsHider.hidden(TrapChance) # type: ignore + Traps: HiddenTraps # type: ignore + DeathLink: OptionsHider.hidden(DeathLink) # type: ignore + has_replaced_options: HasReplacedCamelCase + + def handle_backward_compatibility(self) -> None: + if self.StartWithJewelryBox != StartWithJewelryBox.default and \ + self.start_with_jewelry_box == StartWithJewelryBox.default: + self.start_with_jewelry_box.value = self.StartWithJewelryBox.value + self.has_replaced_options.value = Toggle.option_true + if self.DownloadableItems != DownloadableItems.default and \ + self.downloadable_items == DownloadableItems.default: + self.downloadable_items.value = self.DownloadableItems.value + self.has_replaced_options.value = Toggle.option_true + if self.EyeSpy != EyeSpy.default and \ + self.eye_spy == EyeSpy.default: + self.eye_spy.value = self.EyeSpy.value + self.has_replaced_options.value = Toggle.option_true + if self.StartWithMeyef != StartWithMeyef.default and \ + self.start_with_meyef == StartWithMeyef.default: + self.start_with_meyef.value = self.StartWithMeyef.value + self.has_replaced_options.value = Toggle.option_true + if self.QuickSeed != QuickSeed.default and \ + self.quick_seed == QuickSeed.default: + self.quick_seed.value = self.QuickSeed.value + self.has_replaced_options.value = Toggle.option_true + if self.SpecificKeycards != SpecificKeycards.default and \ + self.specific_keycards == SpecificKeycards.default: + self.specific_keycards.value = self.SpecificKeycards.value + self.has_replaced_options.value = Toggle.option_true + if self.Inverted != Inverted.default and \ + self.inverted == Inverted.default: + self.inverted.value = self.Inverted.value + self.has_replaced_options.value = Toggle.option_true + if self.GyreArchives != GyreArchives.default and \ + self.gyre_archives == GyreArchives.default: + self.gyre_archives.value = self.GyreArchives.value + self.has_replaced_options.value = Toggle.option_true + if self.Cantoran != Cantoran.default and \ + self.cantoran == Cantoran.default: + self.cantoran.value = self.Cantoran.value + self.has_replaced_options.value = Toggle.option_true + if self.LoreChecks != LoreChecks.default and \ + self.lore_checks == LoreChecks.default: + self.lore_checks.value = self.LoreChecks.value + self.has_replaced_options.value = Toggle.option_true + if self.BossRando != BossRando.default and \ + self.boss_rando == BossRando.default: + self.boss_rando.value = self.BossRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRando != DamageRando.default and \ + self.damage_rando == DamageRando.default: + self.damage_rando.value = self.DamageRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRandoOverrides != DamageRandoOverrides.default and \ + self.damage_rando_overrides == DamageRandoOverrides.default: + self.damage_rando_overrides.value = self.DamageRandoOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.HpCap != HpCap.default and \ + self.hp_cap == HpCap.default: + self.hp_cap.value = self.HpCap.value + self.has_replaced_options.value = Toggle.option_true + if self.LevelCap != LevelCap.default and \ + self.level_cap == LevelCap.default: + self.level_cap.value = self.LevelCap.value + self.has_replaced_options.value = Toggle.option_true + if self.ExtraEarringsXP != ExtraEarringsXP.default and \ + self.extra_earrings_xp == ExtraEarringsXP.default: + self.extra_earrings_xp.value = self.ExtraEarringsXP.value + self.has_replaced_options.value = Toggle.option_true + if self.BossHealing != BossHealing.default and \ + self.boss_healing == BossHealing.default: + self.boss_healing.value = self.BossHealing.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopFill != ShopFill.default and \ + self.shop_fill == ShopFill.default: + self.shop_fill.value = self.ShopFill.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopWarpShards != ShopWarpShards.default and \ + self.shop_warp_shards == ShopWarpShards.default: + self.shop_warp_shards.value = self.ShopWarpShards.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopMultiplier != ShopMultiplier.default and \ + self.shop_multiplier == ShopMultiplier.default: + self.shop_multiplier.value = self.ShopMultiplier.value + self.has_replaced_options.value = Toggle.option_true + if self.LootPool != LootPool.default and \ + self.loot_pool == LootPool.default: + self.loot_pool.value = self.LootPool.value + self.has_replaced_options.value = Toggle.option_true + if self.DropRateCategory != DropRateCategory.default and \ + self.drop_rate_category == DropRateCategory.default: + self.drop_rate_category.value = self.DropRateCategory.value + self.has_replaced_options.value = Toggle.option_true + if self.FixedDropRate != FixedDropRate.default and \ + self.fixed_drop_rate == FixedDropRate.default: + self.fixed_drop_rate.value = self.FixedDropRate.value + self.has_replaced_options.value = Toggle.option_true + if self.LootTierDistro != LootTierDistro.default and \ + self.loot_tier_distro == LootTierDistro.default: + self.loot_tier_distro.value = self.LootTierDistro.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowBestiary != ShowBestiary.default and \ + self.show_bestiary == ShowBestiary.default: + self.show_bestiary.value = self.ShowBestiary.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowDrops != ShowDrops.default and \ + self.show_drops == ShowDrops.default: + self.show_drops.value = self.ShowDrops.value + self.has_replaced_options.value = Toggle.option_true + if self.EnterSandman != EnterSandman.default and \ + self.enter_sandman == EnterSandman.default: + self.enter_sandman.value = self.EnterSandman.value + self.has_replaced_options.value = Toggle.option_true + if self.DadPercent != DadPercent.default and \ + self.dad_percent == DadPercent.default: + self.dad_percent.value = self.DadPercent.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTides != RisingTides.default and \ + self.rising_tides == RisingTides.default: + self.rising_tides.value = self.RisingTides.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTidesOverrides != RisingTidesOverrides.default and \ + self.rising_tides_overrides == RisingTidesOverrides.default: + self.rising_tides_overrides.value = self.RisingTidesOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.UnchainedKeys != UnchainedKeys.default and \ + self.unchained_keys == UnchainedKeys.default: + self.unchained_keys.value = self.UnchainedKeys.value + self.has_replaced_options.value = Toggle.option_true + if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \ + self.back_to_the_future == PresentAccessWithWheelAndSpindle.default: + self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value + self.has_replaced_options.value = Toggle.option_true + if self.TrapChance != TrapChance.default and \ + self.trap_chance == TrapChance.default: + self.trap_chance.value = self.TrapChance.value + self.has_replaced_options.value = Toggle.option_true + if self.Traps != Traps.default and \ + self.traps == Traps.default: + self.traps.value = self.Traps.value + self.has_replaced_options.value = Toggle.option_true + if self.DeathLink != DeathLink.default and \ + self.death_link == DeathLink.default: + self.death_link.value = self.DeathLink.value + self.has_replaced_options.value = Toggle.option_true diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index ff7f031d3b67..c9d80d7a709d 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,6 +1,6 @@ from typing import Tuple, Dict, Union, List -from BaseClasses import MultiWorld -from .Options import timespinner_options, is_option_enabled, get_option_value +from random import Random +from .Options import TimespinnerOptions class PreCalculatedWeights: pyramid_keys_unlock: str @@ -21,22 +21,22 @@ class PreCalculatedWeights: flood_lake_serene_bridge: bool flood_lab: bool - def __init__(self, world: MultiWorld, player: int): - if world and is_option_enabled(world, player, "RisingTides"): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + def __init__(self, options: TimespinnerOptions, random: Random): + if options.rising_tides: + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options) self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") - self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") + self.roll_flood_setting(random, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation") + self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene") + self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge") + self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab") else: self.flood_basement = False self.flood_basement_high = False @@ -52,10 +52,12 @@ def __init__(self, world: MultiWorld, player: int): self.flood_lab = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw, self.flood_xarion) + self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random, + is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: List[str] = [ "GateKittyBoss", "GateLeftLibrary", @@ -80,38 +82,30 @@ def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: boo "GateRightPyramid" ) - if not world: - return ( - present_teleportation_gates[0], - present_teleportation_gates[0], - past_teleportation_gates[0], - ancient_pyramid_teleportation_gates[0] - ) - if not is_maw_flooded: past_teleportation_gates.append("GateMaw") if not is_xarion_flooded: present_teleportation_gates.append("GateXarion") - if is_option_enabled(world, player, "Inverted"): + if options.inverted: all_gates: Tuple[str, ...] = present_teleportation_gates else: all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates return ( - world.random.choice(all_gates), - world.random.choice(present_teleportation_gates), - world.random.choice(past_teleportation_gates), - world.random.choice(ancient_pyramid_teleportation_gates) + random.choice(all_gates), + random.choice(present_teleportation_gates), + random.choice(past_teleportation_gates), + random.choice(ancient_pyramid_teleportation_gates) ) @staticmethod - def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ - get_option_value(world, player, "RisingTidesOverrides") + options.rising_tides_overrides.value - default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + default_weights: Dict[str, Dict[str, int]] = options.rising_tides_overrides.default if not weights_overrides_option: weights_overrides_option = default_weights @@ -123,13 +117,13 @@ def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Uni return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, - all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: + def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]], + key: str) -> Tuple[bool, bool]: weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] else: result: str = weights diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 757a41c38821..f737b461d0bc 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,14 +1,16 @@ from typing import List, Set, Dict, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location -from .Options import is_option_enabled +from .Options import TimespinnerOptions from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): +def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions, + precalculated_weights: PreCalculatedWeights): + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( - get_location_datas(world, player, precalculated_weights)) + get_location_datas(player, options, precalculated_weights)) regions = [ create_region(world, player, locations_per_region, 'Menu'), @@ -53,7 +55,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'Space time continuum') ] - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: regions.extend([ create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), @@ -64,10 +66,10 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w world.regions += regions - connectStartingRegion(world, player) + connectStartingRegion(world, player, options) flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) + logic = TimespinnerLogic(player, options, precalculated_weights) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") @@ -123,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) + connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.back_to_the_future and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) @@ -178,11 +180,11 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not options.unchained_keys and options.enter_sandman)) connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp") @@ -220,12 +222,12 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str return region -def connectStartingRegion(world: MultiWorld, player: int): +def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions): menu = world.get_region('Menu', player) tutorial = world.get_region('Tutorial', player) space_time_continuum = world.get_region('Space time continuum', player) - if is_option_enabled(world, player, "Inverted"): + if options.inverted: starting_region = world.get_region('Refugee Camp', player) else: starting_region = world.get_region('Lake desolation', player) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cab6fb648b95..66744cffdf85 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,12 +1,13 @@ -from typing import Dict, List, Set, Tuple, TextIO, Union -from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from typing import Dict, List, Set, Tuple, TextIO +from BaseClasses import Item, Tutorial, ItemClassification from .Items import get_item_names_per_category from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items from .Locations import get_location_datas, EventId -from .Options import is_option_enabled, get_option_value, timespinner_options +from .Options import BackwardsCompatiableTimespinnerOptions, Toggle from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld +import logging class TimespinnerWebWorld(WebWorld): theme = "ice" @@ -35,32 +36,34 @@ class TimespinnerWorld(World): Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. """ - - option_definitions = timespinner_options + options_dataclass = BackwardsCompatiableTimespinnerOptions + options: BackwardsCompatiableTimespinnerOptions game = "Timespinner" topology_present = True web = TimespinnerWebWorld() required_client_version = (0, 4, 2) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} + location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)} item_name_groups = get_item_names_per_category() precalculated_weights: PreCalculatedWeights def generate_early(self) -> None: - self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) + self.options.handle_backward_compatibility() + + self.precalculated_weights = PreCalculatedWeights(self.options, self.random) # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly - if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: - self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0: - self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: - self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true + if self.options.start_inventory.value.pop('Meyef', 0) > 0: + self.options.start_with_meyef.value = Toggle.option_true + if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0: + self.options.quick_seed.value = Toggle.option_true + if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: + self.options.start_with_jewelry_box.value = Toggle.option_true def create_regions(self) -> None: - create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) + create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) def create_items(self) -> None: self.create_and_assign_event_items() @@ -74,7 +77,7 @@ def create_items(self) -> None: def set_rules(self) -> None: final_boss: str - if self.is_option_enabled("DadPercent"): + if self.options.dad_percent: final_boss = "Killed Emperor" else: final_boss = "Killed Nightmare" @@ -82,48 +85,74 @@ def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) def fill_slot_data(self) -> Dict[str, object]: - slot_data: Dict[str, object] = {} - - ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"} - - for option_name in timespinner_options: - if (option_name not in ap_specific_settings): - slot_data[option_name] = self.get_option_value(option_name) - - slot_data["StinkyMaw"] = True - slot_data["ProgressiveVerticalMovement"] = False - slot_data["ProgressiveKeycards"] = False - slot_data["PersonalItems"] = self.get_personal_items() - slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock - slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock - slot_data["PastGate"] = self.precalculated_weights.past_key_unlock - slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock - slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \ - int(self.precalculated_weights.flood_basement_high) - slot_data["Xarion"] = self.precalculated_weights.flood_xarion - slot_data["Maw"] = self.precalculated_weights.flood_maw - slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft - slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back - slot_data["CastleMoat"] = self.precalculated_weights.flood_moat - slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard - slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation - slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene - slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge - slot_data["Lab"] = self.precalculated_weights.flood_lab - - return slot_data + return { + # options + "StartWithJewelryBox": self.options.start_with_jewelry_box.value, + "DownloadableItems": self.options.downloadable_items.value, + "EyeSpy": self.options.eye_spy.value, + "StartWithMeyef": self.options.start_with_meyef.value, + "QuickSeed": self.options.quick_seed.value, + "SpecificKeycards": self.options.specific_keycards.value, + "Inverted": self.options.inverted.value, + "GyreArchives": self.options.gyre_archives.value, + "Cantoran": self.options.cantoran.value, + "LoreChecks": self.options.lore_checks.value, + "BossRando": self.options.boss_rando.value, + "DamageRando": self.options.damage_rando.value, + "DamageRandoOverrides": self.options.damage_rando_overrides.value, + "HpCap": self.options.hp_cap.value, + "LevelCap": self.options.level_cap.value, + "ExtraEarringsXP": self.options.extra_earrings_xp.value, + "BossHealing": self.options.boss_healing.value, + "ShopFill": self.options.shop_fill.value, + "ShopWarpShards": self.options.shop_warp_shards.value, + "ShopMultiplier": self.options.shop_multiplier.value, + "LootPool": self.options.loot_pool.value, + "DropRateCategory": self.options.drop_rate_category.value, + "FixedDropRate": self.options.fixed_drop_rate.value, + "LootTierDistro": self.options.loot_tier_distro.value, + "ShowBestiary": self.options.show_bestiary.value, + "ShowDrops": self.options.show_drops.value, + "EnterSandman": self.options.enter_sandman.value, + "DadPercent": self.options.dad_percent.value, + "RisingTides": self.options.rising_tides.value, + "UnchainedKeys": self.options.unchained_keys.value, + "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, + "Traps": self.options.traps.value, + "DeathLink": self.options.death_link.value, + "StinkyMaw": True, + # data + "PersonalItems": self.get_personal_items(), + "PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock, + "PresentGate": self.precalculated_weights.present_key_unlock, + "PastGate": self.precalculated_weights.past_key_unlock, + "TimeGate": self.precalculated_weights.time_key_unlock, + # rising tides + "Basement": int(self.precalculated_weights.flood_basement) + \ + int(self.precalculated_weights.flood_basement_high), + "Xarion": self.precalculated_weights.flood_xarion, + "Maw": self.precalculated_weights.flood_maw, + "PyramidShaft": self.precalculated_weights.flood_pyramid_shaft, + "BackPyramid": self.precalculated_weights.flood_pyramid_back, + "CastleMoat": self.precalculated_weights.flood_moat, + "CastleCourtyard": self.precalculated_weights.flood_courtyard, + "LakeDesolation": self.precalculated_weights.flood_lake_desolation, + "DryLakeSerene": not self.precalculated_weights.flood_lake_serene, + "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge, + "Lab": self.precalculated_weights.flood_lab + } def write_spoiler_header(self, spoiler_handle: TextIO) -> None: - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') - if self.is_option_enabled("EnterSandman"): + if self.options.enter_sandman: spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - if self.is_option_enabled("RisingTides"): + if self.options.rising_tides: flooded_areas: List[str] = [] if self.precalculated_weights.flood_basement: @@ -159,6 +188,15 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') + if self.options.has_replaced_options: + warning = \ + f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ + "please update your yaml" + + spoiler_handle.write("\n") + spoiler_handle.write(warning) + logging.warning(warning) + def create_item(self, name: str) -> Item: data = item_table[name] @@ -176,41 +214,41 @@ def create_item(self, name: str) -> Item: if not item.advancement: return item - if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): + if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items: item.classification = ItemClassification.filler - elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): + elif name == 'Oculus Ring' and not self.options.eye_spy: item.classification = ItemClassification.filler - elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): + elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives: item.classification = ItemClassification.filler elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ - and not self.is_option_enabled("UnchainedKeys"): + and not self.options.unchained_keys: item.classification = ItemClassification.filler return item def get_filler_item_name(self) -> str: - trap_chance: int = self.get_option_value("TrapChance") - enabled_traps: List[str] = self.get_option_value("Traps") + trap_chance: int = self.options.trap_chance.value + enabled_traps: List[str] = self.options.traps.value - if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: - return self.multiworld.random.choice(enabled_traps) + if self.random.random() < (trap_chance / 100) and enabled_traps: + return self.random.choice(enabled_traps) else: - return self.multiworld.random.choice(filler_items) + return self.random.choice(filler_items) def get_excluded_items(self) -> Set[str]: excluded_items: Set[str] = set() - if self.is_option_enabled("StartWithJewelryBox"): + if self.options.start_with_jewelry_box: excluded_items.add('Jewelry Box') - if self.is_option_enabled("StartWithMeyef"): + if self.options.start_with_meyef: excluded_items.add('Meyef') - if self.is_option_enabled("QuickSeed"): + if self.options.quick_seed: excluded_items.add('Talaria Attachment') - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: excluded_items.add('Twin Pyramid Key') - if not self.is_option_enabled("EnterSandman"): + if not self.options.enter_sandman: excluded_items.add('Mysterious Warp Beacon') else: excluded_items.add('Timeworn Warp Beacon') @@ -224,8 +262,8 @@ def get_excluded_items(self) -> Set[str]: return excluded_items def assign_starter_items(self, excluded_items: Set[str]) -> None: - non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value - local_items: Set[str] = self.multiworld.local_items[self.player].value + non_local_items: Set[str] = self.options.non_local_items.value + local_items: Set[str] = self.options.local_items.value local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) @@ -247,27 +285,26 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: - item_name = self.multiworld.random.choice(item_list) + item_name = self.random.choice(item_list) self.place_locked_item(excluded_items, location, item_name) def place_first_progression_item(self, excluded_items: Set[str]) -> None: - if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ - or self.precalculated_weights.flood_lake_desolation: + if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation: return - for item in self.multiworld.precollected_items[self.player]: - if item.name in starter_progression_items and not item.name in excluded_items: + for item_name in self.options.start_inventory.value.keys(): + if item_name in starter_progression_items: return local_starter_progression_items = tuple( item for item in starter_progression_items - if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) + if item not in excluded_items and item not in self.options.non_local_items.value) if not local_starter_progression_items: return - progression_item = self.multiworld.random.choice(local_starter_progression_items) + progression_item = self.random.choice(local_starter_progression_items) self.multiworld.local_early_items[self.player][progression_item] = 1 @@ -307,9 +344,3 @@ def get_personal_items(self) -> Dict[int, int]: personal_items[location.address] = location.item.code return personal_items - - def is_option_enabled(self, option: str) -> bool: - return is_option_enabled(self.multiworld, self.player, option) - - def get_option_value(self, option: str) -> Union[int, Dict, List]: - return get_option_value(self.multiworld, self.player, option) From 83521e99d9c78f3240f4bba1447d13f19ca34681 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 05:04:21 -0500 Subject: [PATCH 108/393] Core: migrate item links out of main (#2914) * Core: move item linking out of main * add a test that item link option correctly validates * remove unused fluff --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 79 ++++++++++++++++++++++++++++++++++++ Main.py | 77 +---------------------------------- test/general/test_options.py | 14 ++++++- 3 files changed, 93 insertions(+), 77 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1c7dad7f3b9e..6456210e95dc 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import itertools import functools @@ -288,6 +289,84 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] + def link_items(self) -> None: + """Called to link together items in the itempool related to the registered item link groups.""" + for group_id, group in self.groups.items(): + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in self.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + classifications[item.name] |= item.classification + + for player in players.copy(): + if all([counters[player][item] == 0 for item in shared_pool]): + players.remove(player) + del (counters[player]) + + if not players: + return None, None + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del (counters[player][item]) + return counters, classifications + + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) + if not common_item_count: + continue + + new_itempool: List[Item] = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_item = group["world"].create_item(item_name) + # mangle together all original classification bits + new_item.classification |= classifications[item_name] + new_itempool.append(new_item) + + region = Region("Menu", group_id, self, "ItemLink") + self.regions.append(region) + locations = region.locations + for item in self.itempool: + count = common_item_count.get(item.player, {}).get(item.name, 0) + if count: + loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ + state.has(item_name, group_id_, count_) + + locations.append(loc) + loc.place_locked_item(item) + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(self.itempool) + self.itempool = new_itempool + + while itemcount > len(self.itempool): + items_to_add = [] + for player in group["players"]: + if group["link_replacement"]: + item_player = group_id + else: + item_player = player + if group["replacement_items"][player]: + items_to_add.append(AutoWorld.call_single(self, "create_item", item_player, + group["replacement_items"][player])) + else: + items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player)) + self.random.shuffle(items_to_add) + self.itempool.extend(items_to_add[:itemcount - len(self.itempool)]) + def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True diff --git a/Main.py b/Main.py index 56b3a6545db2..ce054dcd393f 100644 --- a/Main.py +++ b/Main.py @@ -184,82 +184,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." multiworld.itempool[:] = new_items - # temporary home for item links, should be moved out of Main - for group_id, group in multiworld.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ - Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] - ]: - classifications: Dict[str, int] = collections.defaultdict(int) - counters = {player: {name: 0 for name in shared_pool} for player in players} - for item in multiworld.itempool: - if item.player in counters and item.name in shared_pool: - counters[item.player][item.name] += 1 - classifications[item.name] |= item.classification - - for player in players.copy(): - if all([counters[player][item] == 0 for item in shared_pool]): - players.remove(player) - del (counters[player]) - - if not players: - return None, None - - for item in shared_pool: - count = min(counters[player][item] for player in players) - if count: - for player in players: - counters[player][item] = count - else: - for player in players: - del (counters[player][item]) - return counters, classifications - - common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) - if not common_item_count: - continue - - new_itempool: List[Item] = [] - for item_name, item_count in next(iter(common_item_count.values())).items(): - for _ in range(item_count): - new_item = group["world"].create_item(item_name) - # mangle together all original classification bits - new_item.classification |= classifications[item_name] - new_itempool.append(new_item) - - region = Region("Menu", group_id, multiworld, "ItemLink") - multiworld.regions.append(region) - locations = region.locations - for item in multiworld.itempool: - count = common_item_count.get(item.player, {}).get(item.name, 0) - if count: - loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", - None, region) - loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ - state.has(item_name, group_id_, count_) - - locations.append(loc) - loc.place_locked_item(item) - common_item_count[item.player][item.name] -= 1 - else: - new_itempool.append(item) - - itemcount = len(multiworld.itempool) - multiworld.itempool = new_itempool - - while itemcount > len(multiworld.itempool): - items_to_add = [] - for player in group["players"]: - if group["link_replacement"]: - item_player = group_id - else: - item_player = player - if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, - group["replacement_items"][player])) - else: - items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) - multiworld.random.shuffle(items_to_add) - multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) + multiworld.link_items() if any(multiworld.item_links.values()): multiworld._all_state = None diff --git a/test/general/test_options.py b/test/general/test_options.py index 6cf642029e65..2229b7ea7e66 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -1,6 +1,6 @@ import unittest -from BaseClasses import PlandoOptions +from BaseClasses import MultiWorld, PlandoOptions from Options import ItemLinks from worlds.AutoWorld import AutoWorldRegister @@ -47,3 +47,15 @@ def test_item_links_name_groups(self): self.assertIn("Bow", link.value[0]["item_pool"]) # TODO test that the group created using these options has the items + + def test_item_links_resolve(self): + """Test item link option resolves correctly.""" + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Everything"], + "link_replacement": False, + "replacement_item": None, + }] + item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} + for link in item_links.values(): + self.assertEqual(link.value[0], item_link_group[0]) From a05dbac55ffdd1e2f2192421b3fba7fca5341c5c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 05:13:14 -0500 Subject: [PATCH 109/393] Core: Rework accessibility (#1481) * rename locations accessibility to "full" and make old locations accessibility debug only * fix a bug in oot * reorder lttp tests to not override its overrides * changed the wrong word in the dict * :forehead: * update the manual lttp yaml * use __debug__ * update pokemon and messenger * fix conflicts from 993 * fix stardew presets * add that locations may be inaccessible to description * use reST format and make the items description one line so that it renders correctly on webhost * forgot i renamed that * add aliases for back compat * some cleanup * fix imports * fix test failure * only check "items" players when the item is progression * Revert "only check "items" players when the item is progression" This reverts commit ecbf986145e6194aa99a39c481d8ecd0736d5a4c. * remove some unnecessary diffs * CV64: Add ItemsAccessibility * put items description at the bottom of the docstring since that's it's visual order * : * rename accessibility reference in pokemon rb dexsanity * make the rendered tooltips look nicer --- BaseClasses.py | 23 ++++++--------- Options.py | 29 +++++++++++++++---- test/general/test_fill.py | 4 +-- test/multiworld/test_multiworlds.py | 2 +- worlds/alttp/Options.py | 5 ++-- worlds/alttp/Rules.py | 17 ++++++----- worlds/alttp/test/inverted/TestInverted.py | 4 +-- .../TestInvertedMinor.py | 2 +- .../test/inverted_owg/TestInvertedOWG.py | 2 +- worlds/cv64/options.py | 4 ++- worlds/ffmq/Regions.py | 2 +- worlds/messenger/options.py | 8 ++--- worlds/minecraft/docs/minecraft_es.md | 2 +- worlds/minecraft/docs/minecraft_sv.md | 2 +- worlds/pokemon_rb/__init__.py | 2 +- worlds/pokemon_rb/options.py | 5 ++-- worlds/pokemon_rb/rules.py | 2 +- worlds/smz3/Options.py | 3 +- worlds/smz3/__init__.py | 2 +- worlds/stardew_valley/presets.py | 12 ++++---- 20 files changed, 75 insertions(+), 57 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6456210e95dc..a0c243c0fd9d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -64,7 +64,6 @@ class MultiWorld(): state: CollectionState plando_options: PlandoOptions - accessibility: Dict[int, Options.Accessibility] early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]] local_items: Dict[int, Options.LocalItems] @@ -602,26 +601,22 @@ def fulfills_accessibility(self, state: Optional[CollectionState] = None): players: Dict[str, Set[int]] = { "minimal": set(), "items": set(), - "locations": set() + "full": set() } - for player, access in self.accessibility.items(): - players[access.current_key].add(player) + for player, world in self.worlds.items(): + players[world.options.accessibility.current_key].add(player) beatable_fulfilled = False - def location_condition(location: Location): + def location_condition(location: Location) -> bool: """Determine if this location has to be accessible, location is already filtered by location_relevant""" - if location.player in players["locations"] or (location.item and location.item.player not in - players["minimal"]): - return True - return False + return location.player in players["full"] or \ + (location.item and location.item.player not in players["minimal"]) - def location_relevant(location: Location): + def location_relevant(location: Location) -> bool: """Determine if this location is relevant to sweep.""" - if location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["locations"] or location.advancement): - return True - return False + return location.progress_type != LocationProgressType.EXCLUDED \ + and (location.player in players["full"] or location.advancement) def all_done() -> bool: """Check if all access rules are fulfilled""" diff --git a/Options.py b/Options.py index b5fb25ea34a0..d5e0ce1a550f 100644 --- a/Options.py +++ b/Options.py @@ -1144,18 +1144,35 @@ def __len__(self) -> int: class Accessibility(Choice): - """Set rules for reachability of your items/locations. + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and 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. + **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_full = 0 option_minimal = 2 alias_none = 2 + alias_locations = 0 + alias_items = 0 + default = 0 + + +class ItemsAccessibility(Accessibility): + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. + + **Minimal:** ensure what is needed to reach your goal can be acquired. + + **Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and + some locations may be inaccessible. + """ + option_items = 1 default = 1 diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 485007ff0d56..db24b706918c 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -174,8 +174,8 @@ def test_minimal_mixed_fill(self): player1 = generate_player_data(multiworld, 1, 3, 3) player2 = generate_player_data(multiworld, 2, 3, 3) - multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal - multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations + multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal + multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_full multiworld.completion_condition[player1.id] = lambda state: True multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 677f0de82930..5289cac6c357 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -69,7 +69,7 @@ def test_two_player_single_game_fills(self) -> None: for world in AutoWorldRegister.world_types.values(): self.multiworld = setup_multiworld([world, world], ()) for world in self.multiworld.worlds.values(): - world.options.accessibility.value = Accessibility.option_locations + world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) with self.subTest("filling multiworld", seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index ee3ebc587ce7..20dd18038a14 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,8 +1,8 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ - StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed +from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \ + PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle from .EntranceShuffle import default_connections, default_dungeon_connections, \ inverted_default_connections, inverted_default_dungeon_connections from .Text import TextTable @@ -743,6 +743,7 @@ class ALttPPlandoTexts(PlandoTexts): alttp_options: typing.Dict[str, type(Option)] = { + "accessibility": ItemsAccessibility, "plando_connections": ALttPPlandoConnections, "plando_texts": ALttPPlandoTexts, "start_inventory_from_pool": StartInventoryPool, diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 67684a6f3ced..f596749ae669 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -2,6 +2,7 @@ import logging from typing import Iterator, Set +from Options import ItemsAccessibility from BaseClasses import Entrance, MultiWorld from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) @@ -39,7 +40,7 @@ def set_rules(world): else: # Set access rules according to max glitches for multiworld progression. # Set accessibility to none, and shuffle assuming the no logic players can always win - world.accessibility[player] = world.accessibility[player].from_text("minimal") + world.accessibility[player].value = ItemsAccessibility.option_minimal world.progression_balancing[player].value = 0 else: @@ -377,7 +378,7 @@ def global_rules(multiworld: MultiWorld, player: int): or state.has("Cane of Somaria", player))) set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) @@ -393,7 +394,7 @@ def global_rules(multiworld: MultiWorld, player: int): if state.has('Hookshot', player) else state._lttp_has_key('Small Key (Swamp Palace)', player, 4)) set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: @@ -423,7 +424,7 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) @@ -522,12 +523,12 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6)) @@ -1200,7 +1201,7 @@ def tr_big_key_chest_keys_needed(state): # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player) - if world.accessibility[player] == 'locations': + if world.accessibility[player] == 'full': if world.big_key_shuffle[player] and can_reach_big_chest: # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', @@ -1214,7 +1215,7 @@ def tr_big_key_chest_keys_needed(state): location.place_locked_item(item) toss_junk_item(world, player) - if world.accessibility[player] != 'locations': + if world.accessibility[player] != 'full': set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player))) diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 0a2aa7a18653..a0a654991b43 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -1,11 +1,11 @@ -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index a8fa5c808c3b..bf25c5c9a164 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -6,7 +6,7 @@ from worlds.alttp.Options import GlitchesRequired from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index bbdf0f792444..1de22b95e593 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -6,7 +6,7 @@ from worlds.alttp.Options import GlitchesRequired from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index 93b417ad26fd..07e86347bda6 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool +from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle, + StartInventoryPool) class CharacterStages(Choice): @@ -521,6 +522,7 @@ class DeathLink(Choice): @dataclass class CV64Options(PerGameCommonOptions): + accessibility: ItemsAccessibility start_inventory_from_pool: StartInventoryPool character_stages: CharacterStages stage_shuffle: StageShuffle diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index f7b9b9eed4d8..c1d3d619ffaa 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -216,7 +216,7 @@ def stage_set_rules(multiworld): multiworld.worlds[player].options.accessibility == "minimal"]) * 3): for player in no_enemies_players: for location in vendor_locations: - if multiworld.worlds[player].options.accessibility == "locations": + if multiworld.worlds[player].options.accessibility == "full": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 1f76dba4894a..59e694cd3963 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -3,15 +3,15 @@ from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ +from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \ PlandoConnections, Range, StartInventoryPool, Toggle, Visibility from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS -class MessengerAccessibility(Accessibility): - default = Accessibility.option_locations +class MessengerAccessibility(ItemsAccessibility): # defaulting to locations accessibility since items makes certain items self-locking - __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") + default = ItemsAccessibility.option_full + __doc__ = ItemsAccessibility.__doc__ class PortalPlando(PlandoConnections): diff --git a/worlds/minecraft/docs/minecraft_es.md b/worlds/minecraft/docs/minecraft_es.md index 3f2df6e7ba76..4f4899212240 100644 --- a/worlds/minecraft/docs/minecraft_es.md +++ b/worlds/minecraft/docs/minecraft_es.md @@ -29,7 +29,7 @@ name: TuNombre game: Minecraft # Opciones compartidas por todos los juegos: -accessibility: locations +accessibility: full progression_balancing: 50 # Opciones Especficicas para Minecraft diff --git a/worlds/minecraft/docs/minecraft_sv.md b/worlds/minecraft/docs/minecraft_sv.md index fd89d681ee37..ab8c1b5d8ea7 100644 --- a/worlds/minecraft/docs/minecraft_sv.md +++ b/worlds/minecraft/docs/minecraft_sv.md @@ -79,7 +79,7 @@ description: Template Name # Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns. name: YourName game: Minecraft -accessibility: locations +accessibility: full progression_balancing: 0 advancement_goal: few: 0 diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 5f527033289a..277d73b2258b 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -443,7 +443,7 @@ def pre_fill(self) -> None: self.multiworld.elite_four_pokedex_condition[self.player].total = \ int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value) - if self.multiworld.accessibility[self.player] == "locations": + if self.multiworld.accessibility[self.player] == "full": balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]] traps = [self.create_item(trap) for trap in item_groups["Traps"]] locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index bd6515913aca..54d486a6cf9f 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,4 @@ -from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink +from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility class GameVersion(Choice): @@ -287,7 +287,7 @@ class AllPokemonSeen(Toggle): class DexSanity(NamedRange): """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to - have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable + have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage of all 151 Pokemon. If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to @@ -861,6 +861,7 @@ class RandomizePokemonPalettes(Choice): pokemon_rb_options = { + "accessibility": ItemsAccessibility, "game_version": GameVersion, "trainer_name": TrainerName, "rival_name": RivalName, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 21dceb75e8df..1d68f3148963 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -22,7 +22,7 @@ def prize_rule(i): item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule - if multiworld.accessibility[player] != "locations": + if multiworld.accessibility[player] != "full": multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item: item.name == "Bike Voucher" and item.player == player) diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index ada463fa3629..8c5efc431f5c 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -128,6 +128,7 @@ class EnergyBeep(DefaultOnToggle): smz3_options: typing.Dict[str, type(Option)] = { + "accessibility": ItemsAccessibility, "sm_logic": SMLogic, "sword_location": SwordLocation, "morph_location": MorphLocation, diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index d78c9f7d8224..690e5172a25c 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -244,7 +244,7 @@ def set_rules(self): set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player])) for loc in region.Locations: l = self.locations[loc.Name] - if self.multiworld.accessibility[self.player] != 'locations': + if self.multiworld.accessibility[self.player] != 'full': l.always_allow = lambda state, item, loc=loc: \ item.game == "SMZ3" and \ loc.alwaysAllow(item.item, state.smz3state[self.player]) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index e663241ac6af..cf6f87a1501c 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -58,7 +58,7 @@ easy_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "very rich", @@ -104,7 +104,7 @@ medium_settings = { "progression_balancing": 25, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "rich", @@ -150,7 +150,7 @@ hard_settings = { "progression_balancing": 0, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_grandpa_evaluation, FarmType.internal_name: "random", StartingMoney.internal_name: "extra", @@ -196,7 +196,7 @@ nightmare_settings = { "progression_balancing": 0, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "vanilla", @@ -242,7 +242,7 @@ short_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_bottom_of_the_mines, FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", @@ -334,7 +334,7 @@ allsanity_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, From 7c8ea34a029e7e2a1683b7a318a65da5028800d3 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:32:17 -0600 Subject: [PATCH 110/393] Shivers: New features and removes two missed options using the old options API (#3287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds an option to have pot pieces placed local/non-local/anywhere Shivers nearly always finishes last in multiworld games due to the fact you need all 20 pot pieces to win and the pot pieces open very few location checks. This option allows the pieces to be placed locally. This should allow Shivers to be finished earlier. * New option: Choose how many ixupi captures are needed for goal completion New option: Choose how many ixupi captures are needed for goal completion * Fixes rule logic for location 'puzzle solved three floor elevator' Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. * Merge branch 'main' of https://github.com/GodlFire/Shivers * Revert "Merge branch 'main' of https://github.com/GodlFire/Shivers" This reverts commit bb08c3f0c2ef148fd24d7c7820cdfe936f7196e2. * Fixes issue with office elevator rule logic. * Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' * Moves plaque location to front for better tracker referencing. * Tiki should be Shaman. * Hanging should be Gallows. * Merrick spelling. * Clarity change. * Changes new option to use new option API Changes new option to use new option API * Added sub regions for Ixupi -Added sub regions for Ixupi and moved ixupi capture checks into the sub region. -Added missing wax capture possible spot in Shaman room * Adds option for ixupi captures to be priority locations Adds option for ixupi captures to be priority locations * Consistency Consistency * Changes ixupi captures priority to default on toggle Changes ixupi captures priority to default on toggle * Docs update -Updated link to randomizer -Update some text to reflect the latest functionality -Replaced 'setting' with 'option' * New features/bug fixes -Adds an option to have completed pots in the item pool -Moved subterranean world information plaque to maze staircase * Cleanup Cleanup * Fixed name for moved location When moving a location and renaming it I forgot to fix the name in a second spot. * Squashed commit of the following: commit 630a3bdfb9414d8c57154f29253fce0cf67b6436 Merge: 8477d3c8 5e579200 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Mon Apr 1 19:08:48 2024 -0600 Merge pull request #10 from ArchipelagoMW/main Merge main into branch commit 5e5792009cd3089ae61c5fdd208de1b79d183cb4 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 12:08:21 2024 -0500 LttP: delete playerSettings.yaml (#3062) commit 9aeeeb077a9e894cd2ace51b58d537bcf7607d5b Author: CaitSith2 Date: Mon Apr 1 06:07:56 2024 -0700 ALttP: Re-mark light/dark world regions after applying plando connections (#2964) commit 35458380e6e08eab85203942b6415fd964907c84 Author: Bryce Wilson Date: Mon Apr 1 07:07:11 2024 -0600 Pokemon Emerald: Fix wonder trade race condition (#2983) commit 4ac1866689d01dc6693866ee8b1236ad6fea114b Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 08:06:31 2024 -0500 ALTTP: Skull Woods Inverted fix (#2980) commit 4aa03da66e1a8c99fc31c163c1a23fb0bd772c15 Author: Fabian Dill Date: Mon Apr 1 15:06:02 2024 +0200 Factorio: fix attempting to create savegame with not filename safe characters (#2842) commit 24a03bc8b6b406c0925eedf415dcef47e17fdbaa Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon Apr 1 08:02:26 2024 -0500 KDL3: fix shuffled animals not actually being random (#3060) commit f813a7005fadb1c56bb93fee6147b63d9df2b720 Author: Aaron Wagener Date: Sun Mar 31 11:11:10 2024 -0500 The Messenger: update docs formatting and fix outdated info (#3033) * The Messenger: update docs formatting and fix outdated info * address review feedback * 120 chars commit 2a0b7e0def5c00cc2ac273b22581b3cde3b6f6a6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Sun Mar 31 09:55:55 2024 -0600 CV64: A couple of very small docs corrections. (#3057) commit 03d47e460e434b897b313c2ba452d785ecbacebe Author: Ixrec Date: Sun Mar 31 16:55:08 2024 +0100 A Short Hike: Clarify installation instructions (#3058) * Clarify installation instructions * don't mention 'config' folder since it isn't created until the game starts commit e546c0f7ff2456ddb919a1b65a437a1c61b07479 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun Mar 31 10:50:31 2024 -0500 Yoshi's Island: add patch suffix (#3061) commit 2ec93ba82a969865a8addc98feb076898978c8e3 Author: Bryce Wilson Date: Sun Mar 31 09:48:59 2024 -0600 Pokemon Emerald: Fix inconsistent location name (#3065) commit 4e3d3963941934c77573e6e0b699edf9e26cd647 Author: Aaron Wagener Date: Sun Mar 31 10:47:11 2024 -0500 The Messenger: Fix precollected notes not being removed from the itempool (#3066) * The Messenger: fix precollected notes not being properly removed from pool * The Messenger: bump required client version commit 72c53513f8bdab5506ffa972c1bf6f8573f097d7 Author: Fabian Dill Date: Sun Mar 31 03:57:59 2024 +0200 WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063) commit b7ac6a4cbd54d5f8e6672e4a6c6ea708e7e6d4de Author: Aaron Wagener Date: Fri Mar 29 20:14:53 2024 -0500 The Messenger: Fix various portal shuffle issues (#2976) * put constants in a bit more sensical order * fix accidental incorrect scoping * fix plando rules not being respected * add docstrings for the plando functions * fix the portal output pools being overwritten * use shuffle and pop instead of removing by content so plando can go to the same area twice * move portal pool rebuilding outside mapping creation * remove plando_connection cleansing since it isn't shared with transition shuffle commit 5f0112e78365d19f04e22af92d6ad1f52d264b1f Author: Zach Parks Date: Fri Mar 29 19:13:51 2024 -0500 Tracker: Add starting inventory to trackers and received items table. (#3051) commit bb481256de2a511d3b114f164061d440026be4c4 Author: Aaron Wagener Date: Thu Mar 28 21:48:40 2024 -0500 Core: Make fill failure error more human parseable (#3023) commit 301d9de9758e360ccec5399f3f9d922f1c034e45 Author: Aaron Wagener Date: Thu Mar 28 19:31:59 2024 -0500 Docs: adding games rework (#2892) * Docs: complete adding games.md rework * remove all the now unused images * review changes * address medic's review * address more comments commit 9dc708978bd00890afcd3426f829a5ac53cbe136 Author: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Thu Mar 28 18:26:58 2024 -0600 Hylics 2: Fix invalid multiworld data, use `self.random` instead of `self.multiworld.random` (#3001) * Hylics 2: Fixes * Rewrite loop commit 4391d1f4c13cdf2295481d8c51f9ef8f58bf8347 Author: Bryce Wilson Date: Thu Mar 28 18:05:39 2024 -0600 Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025) commit 5d9d4ed9f1e44309f1b53f12413ad260f1b6c983 Author: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri Mar 29 01:01:31 2024 +0100 SoE: update to pyevermizer v0.48.0 (#3050) commit c97215e0e755224593fdd00894731b59aa415e19 Author: Scipio Wright Date: Thu Mar 28 17:23:37 2024 -0400 TUNIC: Minor refactor of the vanilla_portals function (#3009) * Remove unused, change an if to an elif * Remove unused import commit eb66886a908ad75bbe71fac9bb81a0177e05e816 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu Mar 28 16:23:01 2024 -0500 SC2: Don't Filter Excluded Victory Locations (#3018) commit de860623d17d274289e3e4ab13650f2382e2e0b8 Author: Fabian Dill Date: Thu Mar 28 22:21:56 2024 +0100 Core: differentiate between unknown worlds and broken worlds in error message (#2903) commit 74b2bf51613a968eb57a5b138a7ad191324b2dd8 Author: Bryce Wilson Date: Thu Mar 28 15:20:55 2024 -0600 Pokemon Emerald: Exclude norman trainer location during norman goal (#3038) commit 74ac66b03228988d0885cff556f962a04873cc54 Author: BadMagic100 Date: Thu Mar 28 08:49:19 2024 -0700 Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 80d7ac416493a540548aad67981202a1483b5e53 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu Mar 28 09:41:32 2024 -0500 KDL3: RC1 Fixes and Enhancement (#3022) * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs commit 77311719fa0fa5b67fe92f437c3cfed16bd5136f Author: Ziktofel Date: Thu Mar 28 15:38:34 2024 +0100 SC2: Fix HERC upgrades (#3044) commit cfc1541be9e92f1f59b21f4a81f96fc88f4d9f7e Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 28 15:19:32 2024 +0100 Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 4d954afd9b2311248083fc389ac737995985be86 Author: Scipio Wright Date: Thu Mar 28 10:11:20 2024 -0400 TUNIC: Add link to AP plando guide to connection plando section of game page (#2993) commit 17748a4bf1cfd5cc11c6596a09ffc1f01434340f Author: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Thu Mar 28 10:00:10 2024 -0400 Launcher, Docs: Update UI and Set-Up Guide to Reference Options (#2950) commit 9182fe563fc18ed4ccaa8370cfed88407140398e Author: Entropynines <163603868+Entropynines@users.noreply.github.com> Date: Thu Mar 28 06:56:35 2024 -0700 README: Remove outdated information about launchers (#2966) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit bcf223081facd030aa706dc7430a72bcf2fdadc9 Author: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu Mar 28 09:54:56 2024 -0400 TLOZ: Fix markdown issue with game info page (#2985) commit fa93488f3fceac6c2f51851766543cab3ba121e6 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 28 09:46:00 2024 -0400 Docs: Consistent naming for "connection plando" (#2994) commit db15dd4bde442aad99048224bdb0d7dc28c26717 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Thu Mar 28 08:45:19 2024 -0500 A Short Hike: Fix incorrect info in docs (#3016) commit 01cdb0d761a82349afaeb7222b4b59cb1766f4a0 Author: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu Mar 28 09:44:23 2024 -0400 SMW: Update World Doc for v2.0 Features (#3034) Co-authored-by: Scipio Wright commit d0ac2b744eac438570e6a2333e76fa212be66534 Author: panicbit Date: Thu Mar 28 10:11:26 2024 +0100 LADX: fix local and non-local instrument placement (#2987) * LADX: fix local and non-local instrument placement * change confusing variable name commit 14f5f0127eb753eaf0431a54bebc82f5e74a1cb9 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:42:35 2024 -0400 Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002) * fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) * add test to ensure fishing rod need fish shop * fishing rod should be indexed from 0 like a mentally sane person would do. * fishing rod 0 isn't real, but it definitely can hurt you. * reeeeeeeee commit cf133dde7275e171d388fb466b9ed719ab7ed7c8 Author: Bryce Wilson Date: Thu Mar 28 02:32:27 2024 -0600 Pokemon Emerald: Fix typo (#3020) commit ca1812181106a3645e7f7af417590024b377b25e Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:27:49 2024 -0400 Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970) commit 1d4512590e0b78355e5c10174a9c6749e1098a72 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed Mar 27 21:09:09 2024 +0100 requirements.txt: _ instead of - to make PyCharm happy (#3043) commit f7b415dab00338443b68eba51f42614fc40b9152 Author: agilbert1412 Date: Tue Mar 26 19:40:58 2024 +0300 Stardew valley: Game version documentation (#2990) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 702f006c848c05b847e85f7dbedeef68b70cdcc6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Tue Mar 26 07:31:36 2024 -0600 CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015) commit 98ce8f8844fd0c62214a5774609382cf6a6bc829 Author: Yussur Mustafa Oraji Date: Tue Mar 26 14:29:25 2024 +0100 sm64ex: New Options API and WebHost fix (#2979) commit ea47b90367b4a220c346d8057f3aeb4207d226a1 Author: Scipio Wright Date: Tue Mar 26 09:25:41 2024 -0400 TUNIC: You can grapple down here without the ladder, neat (#3019) commit bf3856866c5ea385d0ac58014c71addfdc92637e Author: agilbert1412 Date: Sun Mar 24 23:53:49 2024 +0300 Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014) commit c0368ae0d48b4b2807c5238aeb7b14937282fc3e Author: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun Mar 24 13:53:20 2024 -0700 SC2: Fixed missing upgrade from custom tracker (#3013) commit 36c83073ad8c2ae1912d390ee3976ba0e2eb3f4a Author: Salzkorn Date: Sun Mar 24 21:52:41 2024 +0100 SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992) commit 2b24539ea5b387a3b62063c8177c373e2e3f8389 Author: Ziktofel Date: Sun Mar 24 21:52:16 2024 +0100 SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986) commit 7e904a1c78c91fb502706fe030a1f1765f734de4 Author: Ziktofel Date: Sun Mar 24 21:51:46 2024 +0100 SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978) commit bdd498db2321417374d572bff8beede083fef2b2 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri Mar 22 15:36:27 2024 -0500 ALTTP: Fix #2290's crashes (#2973) commit 355223b8f0af1ee729ffa8b53eb717aa5bf283a4 Author: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Fri Mar 22 15:35:00 2024 -0500 Yoshi's Island: Implement New Game (#2141) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit aaa3472d5d8d8a7a710bd38386d9eb34046a5578 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri Mar 22 21:30:51 2024 +0100 The Witness: Fix seed bleed issue (#3008) commit 96d93c1ae313bb031e983c0d40d8be199b302df1 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Fri Mar 22 15:30:23 2024 -0500 A Short Hike: Add option to customize filler coin count (#3004) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit ca549df20a0a07c30ee2e1bbc2498492b919604d Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 15:29:24 2024 -0500 CommonClient: fix hint tab overlapping (#2957) Co-authored-by: Remy Jette commit 44988d430dc7d91eaeac7aad681dc024bc19ccce Author: Star Rauchenberger Date: Fri Mar 22 15:28:41 2024 -0500 Lingo: Add trap weights option (#2837) commit 11b32f17abebc08a6140506a375179f8a46bcfe6 Author: Danaël V <104455676+ReverM@users.noreply.github.com> Date: Fri Mar 22 12:46:14 2024 -0400 Docs: replacing "setting" to "option" in world docs (#2622) * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md Added non-AP World specific information * Update contributing.md Fixed broken link * Some minor touchups * Update Contributing.md Draft for version with picture * Update contributing.md Small word change * Minor updates for conciseness, mostly * Changed all instances of settings to options in info and setup guides I combed through all world docs and swapped "setting" to "option" when this was refering to yaml options. I also changed a leftover "setting" in option.py * Update contributing.md * Update contributing.md * Update setup_en.md Woops I forgot one * Update Options.py Reverted changes regarding options.py * Update worlds/noita/docs/en_Noita.md Co-authored-by: Scipio Wright * Update worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md revert change waiting for that page to be updated * Update worlds/witness/docs/setup_en.md * Update worlds/witness/docs/en_The Witness.md * Update worlds/soe/docs/multiworld_en.md Fixed Typo Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/witness/docs/en_The Witness.md * Update worlds/adventure/docs/en_Adventure.md * Update worlds/witness/docs/setup_en.md * Updated Stardew valley to hopefully get rid of the merge conflicts * Didn't work :dismay: * Delete worlds/sc2wol/docs/setup_en.md I think this will fix the merge issue * Now it should work * Woops --------- Co-authored-by: Scipio Wright Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 218cd45844f9d733618af9088941156cd79b80bc Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 03:02:38 2024 -0500 APProcedurePatch: fix RLE/COPY incorrect sizing (#3006) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead * Update Files.py --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 4196bde597cdbb6186ff614294fd54ff043a0c99 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 21 16:38:36 2024 -0400 Docs: Fixing special_range_names example (#3005) commit 40f843f54d5970302caeb2a21b76a4845cf5c0ed Author: Star Rauchenberger Date: Thu Mar 21 11:00:53 2024 -0500 Lingo: Minor game data fixes (#3003) commit da333fbb0c88feedd4821a7bade3f56028a02111 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Thu Mar 21 09:52:16 2024 -0600 Shivers: Adds missing logic rule for skull dial door location (#2997) commit 43084da23c719133fcae672e20c9b046e6ef8067 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 21 16:51:29 2024 +0100 The Witness: Fix newlines in Witness option tooltips (#2971) commit 14816743fca366b52422ccb19add59d4960f17a3 Author: Scipio Wright Date: Thu Mar 21 11:50:07 2024 -0400 TUNIC: Shuffle Ladders option (#2919) commit 30a0aa2c85a7015e2072b5781ed1078965f62f4b Author: Star Rauchenberger Date: Thu Mar 21 10:46:53 2024 -0500 Lingo: Add item/location groups (#2789) commit f4b7c28a33bb163768871616023a8cf3879840b4 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed Mar 20 17:45:32 2024 -0500 APProcedurePatch: hotfix changing class variables to instance variables (#2996) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 12864f7b24028fa56135e599f0fe1642c9d2d377 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Wed Mar 20 22:44:09 2024 +0100 A Short Hike: Implement New Game (#2577) commit db02e9d2aabc0f4c1302ac761b3f5547ef00c7c5 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Wed Mar 20 15:03:25 2024 -0600 Castlevania 64: Implement New Game (#2472) commit 32315776ac0ac1a714eb9d58688c479e2038c658 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Wed Mar 20 16:57:45 2024 -0400 Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967) commit e9620bea777ff1008a09c24a70bf523c94f22c29 Author: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Wed Mar 20 16:56:00 2024 -0400 SM64: Goal Logic and Hint Bugfixes (#2886) commit 183ca35bbaf6c805fdb53396d21d0cba34f9cc5e Author: qwint Date: Wed Mar 20 08:39:37 2024 -0500 CommonClient: Port Casting Bug (#2975) commit fcaaa197a19a3be03965c504ca78dd2c21ce1f84 Author: TheLX5 Date: Wed Mar 20 05:56:19 2024 -0700 SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981) commit 8f7b63a787a0ef05625ae2fad1768251aced0c87 Author: TheLX5 Date: Wed Mar 20 05:56:04 2024 -0700 SMW: Blocksanity logic fixes (#2988) commit 6f64bb98693556ac2635791381cc9651c365b324 Author: Scipio Wright Date: Wed Mar 20 08:46:31 2024 -0400 Noita: Remove newline from option description so it doesn't look bad on webhost (#2969) commit d0a9d0e2d1df641668f4f806b45f9577e69229f6 Author: Bryce Wilson Date: Wed Mar 20 06:43:13 2024 -0600 Pokemon Emerald: Bump required client version (#2963) commit 94650a02de62956eee8e7e41f61e8a41506b5842 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue Mar 19 17:08:29 2024 -0500 Core: implement APProcedurePatch and APTokenMixin (#2536) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * ensure returning bytes, add token type checking * Apply suggestions from code review Co-authored-by: Doug Hoskisson * pep8 --------- Co-authored-by: beauxq Co-authored-by: Doug Hoskisson * Changes pot_completed_list to a instance variable instead of global. Changes pot_completed_list to a instance variable instead of global. The global variable was unintentional and was causing missmatch in pre_fill which would cause generation error. * Removing deprecated options getter * Adds back fix from main branch Adds back fix from main branch * Removing messenger changes that somehow got on my branch? Removing messenger changes that somehow got on my branch? * Removing messenger changes that are somehow on the Shivers branch Removing messenger changes that are somehow on the Shivers branch * Still trying to remove Messenger changes on Shivers branch Still trying to remove Messenger changes on Shivers branch * Review comments addressed. Early lobby access set as default. Review comments addressed. Early lobby access set as default. * Review comments addressed Review comments addressed * Review comments addressed. Option for priority locations removed. Option to have ixupi captures a priority has been removed and can be added again if Priority Fill is changed. See Issues #3467. * Minor Change Minor Change * Fixed ID 10 T Error Fixed ID 10 T Error * Front door option added to slot data Front door option added to slot data * Add missing .value on slot data Add missing .value on slot data * Small change to slot data Small change to slot data * Small change to slot data Why didn't this change get pushed github... * Forgot list Forgot list --------- Co-authored-by: Kory Dondzila Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/shivers/Items.py | 60 ++++++++++++++++-------- worlds/shivers/Options.py | 63 ++++++++++++++++++++++---- worlds/shivers/Rules.py | 55 ++++++++++++---------- worlds/shivers/__init__.py | 73 +++++++++++++++++++++++++----- worlds/shivers/data/locations.json | 46 +++++++++++++------ worlds/shivers/data/regions.json | 72 +++++++++++++++++++++-------- worlds/shivers/docs/en_Shivers.md | 8 ++-- worlds/shivers/docs/setup_en.md | 2 +- 8 files changed, 276 insertions(+), 103 deletions(-) diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py index 3b403be5cb76..10d234d450bb 100644 --- a/worlds/shivers/Items.py +++ b/worlds/shivers/Items.py @@ -33,28 +33,38 @@ class ItemData(typing.NamedTuple): "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"), "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"), "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"), + "Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"), + "Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"), + "Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"), + "Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"), + "Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"), + "Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"), + "Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"), + "Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"), + "Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"), + "Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"), #Keys - "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "key"), - "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "key"), - "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "key"), - "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "key"), - "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "key"), - "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "key"), - "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "key"), - "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "key"), - "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "key"), - "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "key"), - "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), - "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), - "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), - "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), - "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), - "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), - "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), - "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), - "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), - "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key-optional"), + "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), + "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), + "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), + "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), + "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), + "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), + "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), + "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), + "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), + "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"), + "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"), + "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"), + "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"), + "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"), + "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"), + "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"), + "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"), + "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"), + "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"), + "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"), #Abilities "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"), @@ -83,6 +93,16 @@ class ItemData(typing.NamedTuple): "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"), "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"), "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"), + "Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"), + "Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"), + "Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"), + "Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"), + "Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"), + "Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"), + "Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"), + "Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"), + "Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"), + "Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"), #Filler "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"), diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index b70882f9a545..2f33eb18e5d1 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -1,21 +1,37 @@ -from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions +from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range from dataclasses import dataclass +class IxupiCapturesNeeded(Range): + """ + Number of Ixupi Captures needed for goal condition. + """ + display_name = "Number of Ixupi Captures Needed" + range_start = 1 + range_end = 10 + default = 10 + class LobbyAccess(Choice): - """Chooses how keys needed to reach the lobby are placed. + """ + Chooses how keys needed to reach the lobby are placed. - Normal: Keys are placed anywhere - Early: Keys are placed early - - Local: Keys are placed locally""" + - Local: Keys are placed locally + """ display_name = "Lobby Access" option_normal = 0 option_early = 1 option_local = 2 + default = 1 class PuzzleHintsRequired(DefaultOnToggle): - """If turned on puzzle hints will be available before the corresponding puzzle is required. For example: The Shaman - Drums puzzle will be placed after access to the security cameras which give you the solution. Turning this off - allows for greater randomization.""" + """ + If turned on puzzle hints/solutions will be available before the corresponding puzzle is required. + + For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution. + + Turning this off allows for greater randomization. + """ display_name = "Puzzle Hints Required" class InformationPlaques(Toggle): @@ -26,7 +42,9 @@ class InformationPlaques(Toggle): display_name = "Include Information Plaques" class FrontDoorUsable(Toggle): - """Adds a key to unlock the front door of the museum.""" + """ + Adds a key to unlock the front door of the museum. + """ display_name = "Front Door Usable" class ElevatorsStaySolved(DefaultOnToggle): @@ -37,7 +55,9 @@ class ElevatorsStaySolved(DefaultOnToggle): display_name = "Elevators Stay Solved" class EarlyBeth(DefaultOnToggle): - """Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.""" + """ + Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle. + """ display_name = "Early Beth" class EarlyLightning(Toggle): @@ -47,9 +67,34 @@ class EarlyLightning(Toggle): """ display_name = "Early Lightning" +class LocationPotPieces(Choice): + """ + Chooses where pot pieces will be located within the multiworld. + - Own World: Pot pieces will be located within your own world + - Different World: Pot pieces will be located in another world + - Any World: Pot pieces will be located in any world + """ + display_name = "Location of Pot Pieces" + option_own_world = 0 + option_different_world = 1 + option_any_world = 2 + +class FullPots(Choice): + """ + Chooses if pots will be in pieces or already completed + - Pieces: Only pot pieces will be added to the item pool + - Complete: Only completed pots will be added to the item pool + - Mixed: Each pot will be randomly chosen to be pieces or already completed. + """ + display_name = "Full Pots" + option_pieces = 0 + option_complete = 1 + option_mixed = 2 + @dataclass class ShiversOptions(PerGameCommonOptions): + ixupi_captures_needed: IxupiCapturesNeeded lobby_access: LobbyAccess puzzle_hints_required: PuzzleHintsRequired include_information_plaques: InformationPlaques @@ -57,3 +102,5 @@ class ShiversOptions(PerGameCommonOptions): elevators_stay_solved: ElevatorsStaySolved early_beth: EarlyBeth early_lightning: EarlyLightning + location_pot_pieces: LocationPotPieces + full_pots: FullPots diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 3dc4f51c47a2..5288fa2c9c3f 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -8,58 +8,58 @@ def water_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or (state.can_reach("Janitor Closet", "Region", player) and cloth_capturable(state, player))) \ - and state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) + return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \ + state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player) def wax_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Library", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) + return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \ + state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player) def ash_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Office", "Region", player) or state.can_reach("Burial", "Region", player)) \ - and state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) + return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \ + state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player) def oil_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Prehistoric", "Region", player) or state.can_reach("Tar River", "Region", player)) \ - and state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) + return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \ + state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player) def cloth_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Egypt", "Region", player) or state.can_reach("Burial", "Region", player) or state.can_reach("Janitor Closet", "Region", player)) \ - and state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) + return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \ + state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player) def wood_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Workshop", "Region", player) or state.can_reach("Blue Maze", "Region", player) or state.can_reach("Gods Room", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) + return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \ + state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player) def crystal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) + return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \ + state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player) def sand_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Greenhouse", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) + return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \ + state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player) def metal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Projector Room", "Region", player) or state.can_reach("Prehistoric", "Region", player) or state.can_reach("Bedroom", "Region", player)) \ - and state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) + return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \ + state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player) def lightning_capturable(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable or state.multiworld.early_lightning[player].value) \ - and state.can_reach("Generator", "Region", player) \ - and state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \ + and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \ + state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player)) def beths_body_available(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable(state, player) or state.multiworld.early_beth[player].value) \ + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \ and state.can_reach("Generator", "Region", player) @@ -123,7 +123,8 @@ def get_rules_lookup(player: int): "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), "To Slide Room": lambda state: all_skull_dials_available(state, player), - "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) + "To Lobby From Slide Room": lambda state: beths_body_available(state, player), + "To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player) }, "locations_required": { "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player), @@ -207,8 +208,10 @@ def set_rules(world: "ShiversWorld") -> None: # 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) + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player) # Filler Item Forbids forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player) @@ -234,4 +237,8 @@ def set_rules(world: "ShiversWorld") -> None: forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player) # Set completion condition - multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) + multiworld.completion_condition[player] = lambda state: (( + water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \ + + oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \ + + crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \ + + lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index e43e91fb5ae3..a2d7bc14644e 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -1,3 +1,4 @@ +from typing import List from .Items import item_table, ShiversItem from .Rules import set_rules from BaseClasses import Item, Tutorial, Region, Location @@ -22,7 +23,7 @@ class ShiversWorld(World): Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual. """ - game: str = "Shivers" + game = "Shivers" topology_present = False web = ShiversWeb() options_dataclass = ShiversOptions @@ -30,7 +31,13 @@ class ShiversWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = Constants.location_name_to_id - + shivers_item_id_offset = 27000 + pot_completed_list: List[int] + + + def generate_early(self): + self.pot_completed_list = [] + def create_item(self, name: str) -> Item: data = item_table[name] return ShiversItem(name, data.classification, data.code, self.player) @@ -78,9 +85,28 @@ def create_items(self) -> None: #Add items to item pool itempool = [] for name, data in item_table.items(): - if data.type in {"pot", "key", "ability", "filler2"}: + if data.type in {"key", "ability", "filler2"}: itempool.append(self.create_item(name)) + # Pot pieces/Completed/Mixed: + for i in range(10): + if self.options.full_pots == "pieces": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + elif self.options.full_pots == "complete": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + else: + # Roll for if pieces or a complete pot will be used. + # Pot Pieces + if self.random.randint(0, 1) == 0: + self.pot_completed_list.append(0) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + # Completed Pot + else: + self.pot_completed_list.append(1) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + #Add Filler itempool += [self.create_item("Easier Lyre") for i in range(9)] @@ -88,7 +114,6 @@ def create_items(self) -> None: filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool) itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)] - #Place library escape items. Choose a location to place the escape item library_region = self.multiworld.get_region("Library", self.player) librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")]) @@ -123,14 +148,14 @@ def create_items(self) -> None: self.multiworld.itempool += itempool #Lobby acess: - if self.options.lobby_access == 1: + if self.options.lobby_access == "early": if lobby_access_keys == 1: self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1 self.multiworld.early_items[self.player]["Key for Office"] = 1 elif lobby_access_keys == 2: self.multiworld.early_items[self.player]["Key for Front Door"] = 1 - if self.options.lobby_access == 2: + if self.options.lobby_access == "local": if lobby_access_keys == 1: self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1 @@ -138,6 +163,12 @@ def create_items(self) -> None: elif lobby_access_keys == 2: self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1 + #Pot piece shuffle location: + if self.options.location_pot_pieces == "own_world": + self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + if self.options.location_pot_pieces == "different_world": + self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + def pre_fill(self) -> None: # Prefills event storage locations with duplicate pots storagelocs = [] @@ -149,7 +180,23 @@ def pre_fill(self) -> None: if loc_name.startswith("Accessible: "): storagelocs.append(self.multiworld.get_location(loc_name, self.player)) - storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + #Pot pieces/Completed/Mixed: + if self.options.full_pots == "pieces": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + elif self.options.full_pots == "complete": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2'] + storageitems += [self.create_item("Empty") for i in range(10)] + else: + for i in range(10): + #Pieces + if self.pot_completed_list[i] == 0: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])] + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])] + #Complete + else: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])] + storageitems += [self.create_item("Empty")] + storageitems += [self.create_item("Empty") for i in range(3)] state = self.multiworld.get_all_state(True) @@ -166,11 +213,13 @@ def pre_fill(self) -> None: def fill_slot_data(self) -> dict: return { - "storageplacements": self.storage_placements, - "excludedlocations": {str(excluded_location).replace('ExcludeLocations(', '').replace(')', '') for excluded_location in self.multiworld.exclude_locations.values()}, - "elevatorsstaysolved": {self.options.elevators_stay_solved.value}, - "earlybeth": {self.options.early_beth.value}, - "earlylightning": {self.options.early_lightning.value}, + "StoragePlacements": self.storage_placements, + "ExcludedLocations": list(self.options.exclude_locations.value), + "IxupiCapturesNeeded": self.options.ixupi_captures_needed.value, + "ElevatorsStaySolved": self.options.elevators_stay_solved.value, + "EarlyBeth": self.options.early_beth.value, + "EarlyLightning": self.options.early_lightning.value, + "FrontDoorUsable": self.options.front_door_usable.value } diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json index 1d62f85d2d1c..64fe3647348d 100644 --- a/worlds/shivers/data/locations.json +++ b/worlds/shivers/data/locations.json @@ -81,7 +81,7 @@ "Information Plaque: (Ocean) Poseidon", "Information Plaque: (Ocean) Colossus of Rhodes", "Information Plaque: (Ocean) Poseidon's Temple", - "Information Plaque: (Underground Maze) Subterranean World", + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Information Plaque: (Underground Maze) Dero", "Information Plaque: (Egypt) Tomb of the Ixupi", "Information Plaque: (Egypt) The Sphinx", @@ -119,16 +119,6 @@ "Outside": [ "Puzzle Solved Gears", "Puzzle Solved Stone Henge", - "Ixupi Captured Water", - "Ixupi Captured Wax", - "Ixupi Captured Ash", - "Ixupi Captured Oil", - "Ixupi Captured Cloth", - "Ixupi Captured Wood", - "Ixupi Captured Crystal", - "Ixupi Captured Sand", - "Ixupi Captured Metal", - "Ixupi Captured Lightning", "Puzzle Solved Office Elevator", "Puzzle Solved Three Floor Elevator", "Puzzle Hint Found: Combo Lock in Mailbox", @@ -182,7 +172,8 @@ "Accessible: Storage: Transforming Mask" ], "Generator": [ - "Final Riddle: Beth's Body Page 17" + "Final Riddle: Beth's Body Page 17", + "Ixupi Captured Lightning" ], "Theater Back Hallways": [ "Puzzle Solved Clock Tower Door" @@ -210,6 +201,7 @@ "Information Plaque: (Ocean) Poseidon's Temple" ], "Maze Staircase": [ + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Puzzle Solved Maze Door" ], "Egypt": [ @@ -305,7 +297,6 @@ ], "Tar River": [ "Accessible: Storage: Tar River", - "Information Plaque: (Underground Maze) Subterranean World", "Information Plaque: (Underground Maze) Dero" ], "Theater": [ @@ -320,6 +311,33 @@ "Skull Dial Bridge": [ "Accessible: Storage: Skull Bridge", "Puzzle Solved Skull Dial Door" + ], + "Water Capture": [ + "Ixupi Captured Water" + ], + "Wax Capture": [ + "Ixupi Captured Wax" + ], + "Ash Capture": [ + "Ixupi Captured Ash" + ], + "Oil Capture": [ + "Ixupi Captured Oil" + ], + "Cloth Capture": [ + "Ixupi Captured Cloth" + ], + "Wood Capture": [ + "Ixupi Captured Wood" + ], + "Crystal Capture": [ + "Ixupi Captured Crystal" + ], + "Sand Capture": [ + "Ixupi Captured Sand" + ], + "Metal Capture": [ + "Ixupi Captured Metal" ] } -} +} diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json index 963d100faddb..aeb5aa737366 100644 --- a/worlds/shivers/data/regions.json +++ b/worlds/shivers/data/regions.json @@ -7,35 +7,35 @@ ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]], ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]], ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]], - ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office"]], - ["Workshop", ["To Office From Workshop"]], + ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]], + ["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]], ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]], - ["Bedroom", ["To Bedroom Elevator From Bedroom"]], - ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby"]], - ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library"]], + ["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]], + ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]], + ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]], ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]], ["Generator", ["To Maintenance Tunnels From Generator"]], ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]], ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]], ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]], ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]], - ["Projector Room", ["To Theater Back Hallways From Projector Room"]], - ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric"]], - ["Greenhouse", ["To Prehistoric From Greenhouse"]], - ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean"]], + ["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]], + ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]], + ["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]], + ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]], ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]], ["Maze", ["To Maze Staircase From Maze", "To Tar River"]], - ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River"]], - ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt"]], - ["Burial", ["To Egypt From Burial", "To Shaman From Burial"]], - ["Shaman", ["To Burial From Shaman", "To Gods Room"]], - ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room"]], - ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi"]], + ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]], + ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]], + ["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]], + ["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]], + ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]], + ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]], ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]], ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]], - ["Janitor Closet", ["To Night Staircase From Janitor Closet"]], + ["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]], ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]], - ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze"]], + ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]], ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]], ["Fortune Teller", ["To Blue Maze From Fortune Teller"]], ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]], @@ -43,7 +43,16 @@ ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]], ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]], ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]], - ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]] + ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]], + ["Water Capture", []], + ["Wax Capture", []], + ["Ash Capture", []], + ["Oil Capture", []], + ["Cloth Capture", []], + ["Wood Capture", []], + ["Crystal Capture", []], + ["Sand Capture", []], + ["Metal Capture", []] ], "mandatory_connections": [ ["To Registry", "Registry"], @@ -140,6 +149,29 @@ ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"], ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"], ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"], - ["To Slide Room", "Slide Room"] + ["To Slide Room", "Slide Room"], + ["To Wax Capture From Library", "Wax Capture"], + ["To Wax Capture From Shaman", "Wax Capture"], + ["To Wax Capture From Anansi", "Wax Capture"], + ["To Water Capture From Lobby", "Water Capture"], + ["To Water Capture From Janitor Closet", "Water Capture"], + ["To Ash Capture From Office", "Ash Capture"], + ["To Ash Capture From Burial", "Ash Capture"], + ["To Oil Capture From Prehistoric", "Oil Capture"], + ["To Oil Capture From Tar River", "Oil Capture"], + ["To Cloth Capture From Egypt", "Cloth Capture"], + ["To Cloth Capture From Burial", "Cloth Capture"], + ["To Cloth Capture From Janitor Closet", "Cloth Capture"], + ["To Wood Capture From Workshop", "Wood Capture"], + ["To Wood Capture From Gods Room", "Wood Capture"], + ["To Wood Capture From Anansi", "Wood Capture"], + ["To Wood Capture From Blue Maze", "Wood Capture"], + ["To Crystal Capture From Lobby", "Crystal Capture"], + ["To Crystal Capture From Ocean", "Crystal Capture"], + ["To Sand Capture From Greenhouse", "Sand Capture"], + ["To Sand Capture From Ocean", "Sand Capture"], + ["To Metal Capture From Bedroom", "Metal Capture"], + ["To Metal Capture From Projector Room", "Metal Capture"], + ["To Metal Capture From Prehistoric", "Metal Capture"] ] -} \ No newline at end of file +} diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md index a92f8a6b7911..2c56152a7a0c 100644 --- a/worlds/shivers/docs/en_Shivers.md +++ b/worlds/shivers/docs/en_Shivers.md @@ -12,8 +12,8 @@ these are randomized. Crawling has been added and is required to use any crawl s ## What is considered a location check in Shivers? -1. All puzzle solves are location checks excluding elevator puzzles. -2. All Ixupi captures are location checks excluding Lightning. +1. All puzzle solves are location checks. +2. All Ixupi captures are location checks. 3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map. 4. Optionally information plaques are location checks. @@ -23,9 +23,9 @@ If the player receives a key then the corresponding door will be unlocked. If th ## What is the victory condition? -Victory is achieved when the player captures Lightning in the generator room. +Victory is achieved when the player has captured the required number Ixupi set in their options. ## Encountered a bug? -Please contact GodlFire on Discord for bugs related to Shivers world generation.\ +Please contact GodlFire on Discord for bugs related to Shivers world generation.
Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer. diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md index 187382ef643c..c53edcdf2b57 100644 --- a/worlds/shivers/docs/setup_en.md +++ b/worlds/shivers/docs/setup_en.md @@ -5,7 +5,7 @@ - [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc - [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later -- [Shivers Randomizer](https://www.speedrun.com/shivers/resources) +- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version ## Setup ScummVM for Shivers From 91f7cf16de7d7ae15bc6a214273fb7f6f878fe0a Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:32:51 -0600 Subject: [PATCH 111/393] Bomb Rush Cyberfunk: Fix Coil quest being in glitched logic too early (#3720) * Update Rules.py * Update Rules.py --- worlds/bomb_rush_cyberfunk/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/bomb_rush_cyberfunk/Rules.py b/worlds/bomb_rush_cyberfunk/Rules.py index f59a4285709d..8f283ee613b7 100644 --- a/worlds/bomb_rush_cyberfunk/Rules.py +++ b/worlds/bomb_rush_cyberfunk/Rules.py @@ -1006,6 +1006,8 @@ def rules(brcworld): lambda state: mataan_challenge2(state, player, limit, glitched)) set_rule(multiworld.get_location("Mataan: Score challenge reward", player), lambda state: mataan_challenge3(state, player)) + set_rule(multiworld.get_location("Mataan: Coil joins the crew", player), + lambda state: mataan_deepest(state, player, limit, glitched)) if photos: set_rule(multiworld.get_location("Mataan: Trash Polo", player), lambda state: camera(state, player)) From 53bc4ffa52578d6c6c4c289cb399ed0900ec4f30 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 10:37:52 -0500 Subject: [PATCH 112/393] Options: Always verify keys for VerifyKeys options (#3280) * Options: Always verify keys for VerifyKeys options * fix PlandoTexts * use OptionError and give a slightly better error message for which option it is * add the player name to the error * don't create an unnecessary list --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Options.py | 38 ++++++++++++++++++-------- worlds/stardew_valley/test/__init__.py | 4 +-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Options.py b/Options.py index d5e0ce1a550f..d040828509d1 100644 --- a/Options.py +++ b/Options.py @@ -786,17 +786,22 @@ class VerifyKeys(metaclass=FreezeValidKeys): verify_location_name: bool = False value: typing.Any - @classmethod - def verify_keys(cls, data: typing.Iterable[str]) -> None: - if cls.valid_keys: - data = set(data) - dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) - extra = dataset - cls._valid_keys + def verify_keys(self) -> None: + if self.valid_keys: + data = set(self.value) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys if extra: - raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " - f"Allowed keys: {cls._valid_keys}.") + raise OptionError( + f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed keys: {self._valid_keys}." + ) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + try: + self.verify_keys() + except OptionError as validation_error: + raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}") if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -833,7 +838,6 @@ def __init__(self, value: typing.Dict[str, typing.Any]): @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: - cls.verify_keys(data) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -879,7 +883,6 @@ def from_text(cls, text: str): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -905,7 +908,6 @@ def from_text(cls, text: str): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -948,6 +950,19 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P self.value = [] logging.warning(f"The plando texts module is turned off, " f"so text for {player_name} will be ignored.") + else: + super().verify(world, player_name, plando_options) + + def verify_keys(self) -> None: + if self.valid_keys: + data = set(text.at for text in self) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys + if extra: + raise OptionError( + f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed placements: {self._valid_keys}." + ) @classmethod def from_any(cls, data: PlandoTextsFromAnyType) -> Self: @@ -971,7 +986,6 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: texts.append(text) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") - cls.verify_keys([text.at for text in texts]) return cls(texts) else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index d077432e24ae..7e82ea91e434 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -6,7 +6,7 @@ from contextlib import contextmanager from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item, ItemClassification +from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item, ItemClassification from Options import VerifyKeys from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld @@ -365,7 +365,7 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp if issubclass(option, VerifyKeys): # Values should already be verified, but just in case... - option.verify_keys(value.value) + value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses) setattr(args, name, {1: value}) multiworld.set_options(args) From 75b8c7891c65b16dfaa5c06abcc6b53e8299d94e Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:40:45 -0400 Subject: [PATCH 113/393] Docs: Add FFMQ French Setup Guide + Minor fixes to English Guide (#3590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add docs * Fix character * Configuration Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * ajuster Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * inclure Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * doublon Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * remplissage Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * autre Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * pouvoir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * mappemonde Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * virgule Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * fournir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * snes9x Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 3 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * options Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * lien Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * de laquelle Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Étape de génération Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 4 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * également Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * adresse Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Connect Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * seed Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Changer fichier yaml pour de configuration * Fix capitalization Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix capitalization 2 Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix typo+Add link to fr/en info page --------- Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/ffmq/__init__.py | 15 +- .../docs/en_Final Fantasy Mystic Quest.md | 3 + .../docs/fr_Final Fantasy Mystic Quest.md | 36 ++++ worlds/ffmq/docs/setup_en.md | 17 +- worlds/ffmq/docs/setup_fr.md | 178 ++++++++++++++++++ 5 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md create mode 100644 worlds/ffmq/docs/setup_fr.md diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index c464203dc6a4..3c58487265a6 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -25,14 +25,25 @@ class FFMQWebWorld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy Mystic Quest with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] - )] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Artea"] + ) + + tutorials = [setup_en, setup_fr] class FFMQWorld(World): diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md index a652d4e5adcd..4e093930739d 100644 --- a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -1,5 +1,8 @@ # Final Fantasy Mystic Quest +## Game page in other languages: +* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr) + ## Where is the options page? The [player options page for this game](../player-options) contains all the options you need to configure and export a diff --git a/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md new file mode 100644 index 000000000000..70c2d938bfc6 --- /dev/null +++ b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md @@ -0,0 +1,36 @@ +# Final Fantasy Mystic Quest + +## Page d'info dans d'autres langues : +* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en) + +## Où se situe la page d'options? + +La [page de configuration](../player-options) contient toutes les options nécessaires pour créer un fichier de configuration. + +## Qu'est-ce qui est rendu aléatoire dans ce jeu? + +Outre les objets mélangés, il y a plusieurs options pour aussi mélanger les villes et donjons, les pièces dans les donjons, les téléporteurs et les champs de bataille. +Il y a aussi plusieurs autres options afin d'ajuster la difficulté du jeu et la vitesse d'une partie. + +## Quels objets et emplacements sont mélangés? + +Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mélangés. Vous pouvez aussi +inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mélangés. + +## Quels objets peuvent être dans les mondes des autres joueurs? + +Tous les objets qui ont été déterminés mélangés dans les options peuvent être placés dans d'autres mondes. + +## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest? + +Les emplacements qui étaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaîtront comme des coffres. +Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage. +Les pièges peuvent apparaître comme des coffres rouges ou bruns. +Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et +la boîte de dialogue vous indiquera avoir reçu un "Archipelago Item". + + +## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il? + +Une boîte de dialogue apparaîtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous êtes +en combat, dans la mappemonde ou dans les menus (à l'exception de lorsque vous fermez le menu). diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md index 35d775f1bc9f..77569c93f0c8 100644 --- a/worlds/ffmq/docs/setup_en.md +++ b/worlds/ffmq/docs/setup_en.md @@ -17,6 +17,12 @@ The Archipelago community cannot supply you with this. ## Installation Procedures +### Linux Setup + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information. You'll likely be looking for the `.AppImage`.** +2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible. + ### Windows Setup 1. Download and install [Archipelago](). **The installer @@ -75,8 +81,7 @@ Manually launch the SNI Client, and run the patched ROM in your chosen software #### With an emulator -When the client launched automatically, SNI should have also automatically launched in the background. If this is its -first time launching, you may be prompted to allow it to communicate through the Windows Firewall. +If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall. ##### snes9x-rr @@ -133,10 +138,10 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor ### Connect to the Archipelago Server -The patch file which launched your client should have automatically connected you to the AP Server. There are a few -reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the -client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it -into the "Server" input field then press enter. +SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to. +If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed). +In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`. +If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`. The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". diff --git a/worlds/ffmq/docs/setup_fr.md b/worlds/ffmq/docs/setup_fr.md new file mode 100644 index 000000000000..12ea41c6b3a0 --- /dev/null +++ b/worlds/ffmq/docs/setup_fr.md @@ -0,0 +1,178 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Logiciels requis + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES + - Un émulateur capable d'éxécuter des scripts Lua + - snes9x-rr de: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Ou, + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle + compatible +- Le fichier ROM de la v1.0 ou v1.1 NA de Final Fantasy Mystic Quest obtenu légalement, sûrement nommé `Final Fantasy - Mystic Quest (U) (V1.0).sfc` ou `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +La communauté d'Archipelago ne peut vous fournir avec ce fichier. + +## Procédure d'installation + +### Installation sur Linux + +1. Téléchargez et installez [Archipelago](). +** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version. Vous voulez probablement le `.AppImage`** +2. L'utilisation de RetroArch ou BizHawk est recommandé pour les utilisateurs linux, puisque snes9x-rr n'est pas compatible. + +### Installation sur Windows + +1. Téléchargez et installez [Archipelago](). +** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version.** +2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme + programme par défaut pour ouvrir vos ROMs. + 1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez. + 2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...** + 3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers `.sfc`** + 4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC** + 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier + devrait se trouver dans le dossier que vous avez extrait à la première étape. + + +## Créer son fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? + +Votre fichier de configuration contient un ensemble d'options de configuration pour indiquer au générateur +comment il devrait générer votre seed. Chaque joueur d'un multiworld devra fournir son propre fichier de configuration. Cela permet +à chaque joueur d'apprécier une expérience personalisée. Les différents joueurs d'un même multiworld +pouront avoir des options de génération différentes. +Vous pouvez lire le [guide pour créer un YAML de base](/tutorial/Archipelago/setup/en) en anglais. + +### Où est-ce que j'obtiens un fichier de configuration ? + +La [page d'options sur le site](/games/Final%20Fantasy%20Mystic%20Quest/player-options) vous permet de choisir vos +options de génération et de les exporter vers un fichier de configuration. +Il vous est aussi possible de trouver le fichier de configuration modèle de Mystic Quest dans votre répertoire d'installation d'Archipelago, +dans le dossier Players/Templates. + +### Vérifier son fichier de configuration + +Si vous voulez valider votre fichier de configuration pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du +[Validateur de YAML](/mysterycheck). + +## Générer une partie pour un joueur + +1. Aller sur la page [Génération de partie](/games/Final%20Fantasy%20Mystic%20Quest/player-options), configurez vos options, + et cliquez sur le bouton "Generate Game". +2. Il vous sera alors présenté une page d'informations sur la seed +3. Cliquez sur le lien "Create New Room". +4. Vous verrez s'afficher la page du server, de laquelle vous pourrez télécharger votre fichier patch `.apmq`. +5. Rendez-vous sur le [site FFMQR](https://ffmqrando.net/Archipelago). +Sur cette page, sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File". +Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer. +6. Puisque cette partie est à un seul joueur, vous n'avez plus besoin du client Archipelago ni du serveur, sentez-vous libre de les fermer. + +## Rejoindre un MultiWorld + +### Obtenir son patch et créer sa ROM + +Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier de configuration à celui qui héberge la partie ou +s'occupe de la génération. Une fois cela fait, l'hôte vous fournira soit un lien pour télécharger votre patch, soit un +fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.apmq`. + +Allez au [site FFMQR](https://ffmqrando.net/Archipelago) et sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File". +Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer. + +Ouvrez le client SNI (sur Windows ArchipelagoSNIClient.exe, sur Linux ouvrez le `.appImage` puis cliquez sur SNI Client), puis ouvrez le ROM téléchargé avec votre émulateur choisi. + +### Se connecter au client + +#### Avec un émulateur + +Quand le client se lance automatiquement, QUsb2Snes devrait également se lancer automatiquement en arrière-plan. Si +c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu +Windows. + +##### snes9x-rr + +1. Chargez votre ROM si ce n'est pas déjà fait. +2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** +3. Cliquez alors sur **New Lua Script Window...** +4. Dans la nouvelle fenêtre, sélectionnez **Browse...** +5. Sélectionnez le fichier connecteur lua fourni avec votre client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur + est 64-bit ou 32-bit. +6. Si vous obtenez une erreur `socket.dll missing` ou une erreur similaire lorsque vous chargez le script lua, vous devez naviguer dans le dossier +contenant le script lua, puis copier le fichier `socket.dll` dans le dossier d'installation de votre emulateur snes9x. + +##### BizHawk + +1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant + ces options de menu : + `Config --> Cores --> SNES --> BSNES` + Une fois le coeur changé, vous devez redémarrer BizHawk. +2. Chargez votre ROM si ce n'est pas déjà fait. +3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console** +4. Cliquez sur le bouton pour ouvrir un nouveau script Lua, soit par le bouton avec un icône "Ouvrir un dossier", + en cliquant `Open Script...` dans le menu Script ou en appuyant sur `ctrl-O`. +5. Sélectionnez le fichier `Connector.lua` inclus avec le client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur + est 64-bit ou 32-bit. Notez que les versions les plus récentes de BizHawk ne sont que 64-bit. + +##### RetroArch 1.10.1 ou plus récent + +Vous ne devez faire ces étapes qu'une fois. À noter que RetroArch 1.9.x ne fonctionnera pas puisqu'il s'agit d'une version moins récente que 1.10.1. + +1. Entrez dans le menu principal de RetroArch. +2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings". +3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16". + Laissez le "Network Command Port" à sa valeur par defaut, qui devrait être 55355. + + +![Capture d'écran du menu Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sélectionnez "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +Lorsque vous chargez un ROM pour Archipelago, assurez vous de toujours sélectionner le coeur **bsnes-mercury**. +Ce sont les seuls coeurs qui permettent à des outils extérieurs de lire les données du ROM. + +#### Avec une solution matérielle + +Ce guide suppose que vous avez téléchargé le bon micro-logiciel pour votre appareil. Si ce n'est pas déjà le cas, faites +le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logiciel approprié +[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée +[sur cette page](http://usb2snes.com/#supported-platforms). + +1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement. +2. Ouvrez votre appareil et chargez le ROM. + +### Se connecter au MultiServer + +Puisque vous avez lancé SNI manuellement, vous devrez probablement lui indiquer l'adresse à laquelle il doit se connecter. +Si le serveur est hébergé sur le site d'Archipelago, vous verrez l'adresse à laquelle vous connecter dans le haut de la page, dernière ligne avant la liste des mondes. +Tapez `/connect adresse` (ou le "adresse" est remplacé par l'adresse archipelago, par exemple `/connect archipelago.gg:12345`) dans la boîte de commande au bas de votre client SNI, ou encore écrivez l'adresse dans la boîte "server" dans le haut du client, puis cliquez `Connect`. +Si le serveur n'est pas hébergé sur le site d'Archipelago, demandez à l'hôte l'adresse du serveur, puis tapez `/connect adresse` (ou "adresse" est remplacé par l'adresse fourni par l'hôte) ou copiez/collez cette adresse dans le champ "Server" puis appuyez sur "Connect". + +Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status: +Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-être rafraîchir la page de +l'interface Web. + +### Jouer au jeu + +Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations +pour avoir rejoint un multiworld ! + +## Héberger un MultiWorld + +La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par +Archipelago. Le processus est relativement simple : + +1. Récupérez les fichiers de configuration (.yaml) des joueurs. +2. Créez une archive zip contenant ces fichiers de configuration. +3. Téléversez l'archive zip sur le lien ci-dessous. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Attendez un moment que la seed soit générée. +5. Lorsque la seed est générée, vous serez redirigé vers une page d'informations "Seed Info". +6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres + joueurs afin qu'ils puissent récupérer leurs patchs. +7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également + fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quelle personne voulant + observer devrait avoir accès à ce lien. +8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer. From 4620493828b73657633f4ec8d94dd91d4049c4e4 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:27:35 -0400 Subject: [PATCH 114/393] Spire: Convert options, clean up random calls, and add DeathLink (#3704) * Convert StS options * probably a bad idea * Update worlds/spire/Options.py Co-authored-by: Scipio Wright --------- Co-authored-by: Kono Tyran Co-authored-by: Scipio Wright --- worlds/spire/Options.py | 25 ++++++++++++++++++------- worlds/spire/__init__.py | 13 ++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py index 76cbc4cf37ae..9c94756600d6 100644 --- a/worlds/spire/Options.py +++ b/worlds/spire/Options.py @@ -1,5 +1,7 @@ import typing -from Options import TextChoice, Option, Range, Toggle +from dataclasses import dataclass + +from Options import TextChoice, Range, Toggle, PerGameCommonOptions class Character(TextChoice): @@ -55,9 +57,18 @@ class Downfall(Toggle): default = 0 -spire_options: typing.Dict[str, type(Option)] = { - "character": Character, - "ascension": Ascension, - "final_act": FinalAct, - "downfall": Downfall, -} +class DeathLink(Range): + """Percentage of health to lose when a death link is received.""" + display_name = "Death Link %" + range_start = 0 + range_end = 100 + default = 0 + + +@dataclass +class SpireOptions(PerGameCommonOptions): + character: Character + ascension: Ascension + final_act: FinalAct + downfall: Downfall + death_link: DeathLink diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 5b0e1e17f23d..a0a6a794d8a9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial from .Items import event_item_pairs, item_pool, item_table from .Locations import location_table -from .Options import spire_options +from .Options import SpireOptions from .Regions import create_regions from .Rules import set_rules from ..AutoWorld import WebWorld, World @@ -27,7 +27,8 @@ class SpireWorld(World): immense power, and Slay the Spire! """ - option_definitions = spire_options + options_dataclass = SpireOptions + options: SpireOptions game = "Slay the Spire" topology_present = False web = SpireWeb() @@ -63,15 +64,13 @@ def create_regions(self): def fill_slot_data(self) -> dict: slot_data = { - 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)) + 'seed': "".join(self.random.choice(string.ascii_letters) for i in range(16)) } - for option_name in spire_options: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value + slot_data.update(self.options.as_dict("character", "ascension", "final_act", "downfall", "death_link")) return slot_data def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) + return self.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): From c0ef02d6faaaae07c467e007133e1dc5408f6e64 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 4 Aug 2024 06:55:34 -0500 Subject: [PATCH 115/393] Core: fix missing import for `MultiWorld.link_items()` (#3731) --- BaseClasses.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a0c243c0fd9d..81601506d084 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -290,6 +290,8 @@ def set_item_links(self): def link_items(self) -> None: """Called to link together items in the itempool related to the registered item link groups.""" + from worlds import AutoWorld + for group_id, group in self.groups.items(): def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] @@ -300,15 +302,15 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ if item.player in counters and item.name in shared_pool: counters[item.player][item.name] += 1 classifications[item.name] |= item.classification - + for player in players.copy(): if all([counters[player][item] == 0 for item in shared_pool]): players.remove(player) del (counters[player]) - + if not players: return None, None - + for item in shared_pool: count = min(counters[player][item] for player in players) if count: @@ -318,11 +320,11 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ for player in players: del (counters[player][item]) return counters, classifications - + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) if not common_item_count: continue - + new_itempool: List[Item] = [] for item_name, item_count in next(iter(common_item_count.values())).items(): for _ in range(item_count): @@ -330,7 +332,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ # mangle together all original classification bits new_item.classification |= classifications[item_name] new_itempool.append(new_item) - + region = Region("Menu", group_id, self, "ItemLink") self.regions.append(region) locations = region.locations @@ -341,16 +343,16 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ None, region) loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ state.has(item_name, group_id_, count_) - + locations.append(loc) loc.place_locked_item(item) common_item_count[item.player][item.name] -= 1 else: new_itempool.append(item) - + itemcount = len(self.itempool) self.itempool = new_itempool - + while itemcount > len(self.itempool): items_to_add = [] for player in group["players"]: From 203c8f4d89d2740ed98e4f3c1358eb7208d96dfa Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:40:16 -0400 Subject: [PATCH 116/393] Pokemon R/B: Removing Floats from NamedRange #3717 --- worlds/pokemon_rb/options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 54d486a6cf9f..9f217e82e646 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -418,10 +418,10 @@ class ExpModifier(NamedRange): """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" display_name = "Exp Modifier" default = 16 - range_start = default / 4 + range_start = default // 4 range_end = 255 special_range_names = { - "half": default / 2, + "half": default // 2, "normal": default, "double": default * 2, "triple": default * 3, @@ -960,4 +960,4 @@ class RandomizePokemonPalettes(Choice): "ice_trap_weight": IceTrapWeight, "randomize_pokemon_palettes": RandomizePokemonPalettes, "death_link": DeathLink -} \ No newline at end of file +} From 98bb8517e1d40ed6f3b4e9a06024c1470e25c014 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:00:33 -0400 Subject: [PATCH 117/393] Docs: Missed Full Accessibility mention/conversion #3734 --- worlds/generic/docs/advanced_settings_en.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 37467eeb468e..2197c0708e9c 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -102,10 +102,10 @@ See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `accessibility` determines the level of access to the game the generation will expect you to have in order to reach - your completion goal. This supports `items`, `locations`, and `minimal` and is set to `locations` by default. - * `locations` will guarantee all locations are accessible in your world. + your completion goal. This supports `full`, `items`, and `minimal` and is set to `full` by default. + * `full` will guarantee all locations are accessible in your world. * `items` will guarantee you can acquire all logically relevant items in your world. Some items, such as keys, may - be self-locking. + be self-locking. This value only exists in and affects some worlds. * `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. From 90446ad1750034c03521884acb22a084995f4e7c Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:39:56 -0400 Subject: [PATCH 118/393] ChecksFinder: Refactor/Cleaning (#3725) * Update ChecksFinder * minor cleanup * Check for compatible name * Enable APWorld * Update setup_en.md * Update en_ChecksFinder.md * The client is getting updated instead * Qwint suggestions, ' -> ", streamline fill_slot_data * Oops, too many refactors --------- Co-authored-by: SunCat --- setup.py | 1 - worlds/checksfinder/Items.py | 19 ++--- worlds/checksfinder/Locations.py | 44 ++---------- worlds/checksfinder/Options.py | 6 -- worlds/checksfinder/Rules.py | 52 +++++--------- worlds/checksfinder/__init__.py | 78 ++++++++------------- worlds/checksfinder/docs/en_ChecksFinder.md | 5 -- worlds/checksfinder/docs/setup_en.md | 34 +++------ 8 files changed, 69 insertions(+), 170 deletions(-) delete mode 100644 worlds/checksfinder/Options.py diff --git a/setup.py b/setup.py index cb4d1a7511b6..0c9ee2c29302 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ "Adventure", "ArchipIDLE", "Archipelago", - "ChecksFinder", "Clique", "Final Fantasy", "Lufia II Ancient Cave", diff --git a/worlds/checksfinder/Items.py b/worlds/checksfinder/Items.py index 2e86267396f9..5f9be79598af 100644 --- a/worlds/checksfinder/Items.py +++ b/worlds/checksfinder/Items.py @@ -3,8 +3,8 @@ class ItemData(typing.NamedTuple): - code: typing.Optional[int] - progression: bool + code: int + progression: bool = True class ChecksFinderItem(Item): @@ -12,16 +12,9 @@ class ChecksFinderItem(Item): item_table = { - "Map Width": ItemData(80000, True), - "Map Height": ItemData(80001, True), - "Map Bombs": ItemData(80002, True), + "Map Width": ItemData(80000), + "Map Height": ItemData(80001), + "Map Bombs": ItemData(80002), } -required_items = { -} - -item_frequencies = { - -} - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items()} diff --git a/worlds/checksfinder/Locations.py b/worlds/checksfinder/Locations.py index 59a96c83ea8a..aefdc3838100 100644 --- a/worlds/checksfinder/Locations.py +++ b/worlds/checksfinder/Locations.py @@ -3,46 +3,14 @@ class AdvData(typing.NamedTuple): - id: typing.Optional[int] - region: str + id: int + region: str = "Board" -class ChecksFinderAdvancement(Location): +class ChecksFinderLocation(Location): game: str = "ChecksFinder" -advancement_table = { - "Tile 1": AdvData(81000, 'Board'), - "Tile 2": AdvData(81001, 'Board'), - "Tile 3": AdvData(81002, 'Board'), - "Tile 4": AdvData(81003, 'Board'), - "Tile 5": AdvData(81004, 'Board'), - "Tile 6": AdvData(81005, 'Board'), - "Tile 7": AdvData(81006, 'Board'), - "Tile 8": AdvData(81007, 'Board'), - "Tile 9": AdvData(81008, 'Board'), - "Tile 10": AdvData(81009, 'Board'), - "Tile 11": AdvData(81010, 'Board'), - "Tile 12": AdvData(81011, 'Board'), - "Tile 13": AdvData(81012, 'Board'), - "Tile 14": AdvData(81013, 'Board'), - "Tile 15": AdvData(81014, 'Board'), - "Tile 16": AdvData(81015, 'Board'), - "Tile 17": AdvData(81016, 'Board'), - "Tile 18": AdvData(81017, 'Board'), - "Tile 19": AdvData(81018, 'Board'), - "Tile 20": AdvData(81019, 'Board'), - "Tile 21": AdvData(81020, 'Board'), - "Tile 22": AdvData(81021, 'Board'), - "Tile 23": AdvData(81022, 'Board'), - "Tile 24": AdvData(81023, 'Board'), - "Tile 25": AdvData(81024, 'Board'), -} - -exclusion_table = { -} - -events_table = { -} - -lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items() if data.id} \ No newline at end of file +base_id = 81000 +advancement_table = {f"Tile {i+1}": AdvData(base_id+i) for i in range(25)} +lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items()} diff --git a/worlds/checksfinder/Options.py b/worlds/checksfinder/Options.py deleted file mode 100644 index a670109362f7..000000000000 --- a/worlds/checksfinder/Options.py +++ /dev/null @@ -1,6 +0,0 @@ -import typing -from Options import Option - - -checksfinder_options: typing.Dict[str, type(Option)] = { -} diff --git a/worlds/checksfinder/Rules.py b/worlds/checksfinder/Rules.py index 38d7d77ad393..8e8809be5c13 100644 --- a/worlds/checksfinder/Rules.py +++ b/worlds/checksfinder/Rules.py @@ -1,44 +1,24 @@ -from ..generic.Rules import set_rule -from BaseClasses import MultiWorld, CollectionState +from worlds.generic.Rules import set_rule +from BaseClasses import MultiWorld -def _has_total(state: CollectionState, player: int, total: int): - return (state.count('Map Width', player) + state.count('Map Height', player) + - state.count('Map Bombs', player)) >= total +items = ["Map Width", "Map Height", "Map Bombs"] # Sets rules on entrances and advancements that are always applied -def set_rules(world: MultiWorld, player: int): - set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1)) - set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2)) - set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3)) - set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4)) - set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5)) - set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6)) - set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7)) - set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8)) - set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9)) - set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10)) - set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11)) - set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12)) - set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13)) - set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14)) - set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15)) - set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16)) - set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17)) - set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18)) - set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19)) - set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20)) +def set_rules(multiworld: MultiWorld, player: int): + for i in range(20): + set_rule(multiworld.get_location(f"Tile {i+6}", player), lambda state, i=i: state.has_from_list(items, player, i+1)) # Sets rules on completion condition -def set_completion_rules(world: MultiWorld, player: int): - - width_req = 10-5 - height_req = 10-5 - bomb_req = 20-5 - completion_requirements = lambda state: \ - state.has("Map Width", player, width_req) and \ - state.has("Map Height", player, height_req) and \ - state.has("Map Bombs", player, bomb_req) - world.completion_condition[player] = lambda state: completion_requirements(state) +def set_completion_rules(multiworld: MultiWorld, player: int): + width_req = 5 # 10 - 5 + height_req = 5 # 10 - 5 + bomb_req = 15 # 20 - 5 + multiworld.completion_condition[player] = lambda state: state.has_all_counts( + { + "Map Width": width_req, + "Map Height": height_req, + "Map Bombs": bomb_req, + }, player) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index c8b9587f8500..e064a1c41947 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -1,9 +1,9 @@ -from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification -from .Items import ChecksFinderItem, item_table, required_items -from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table -from .Options import checksfinder_options +from BaseClasses import Region, Entrance, Tutorial, ItemClassification +from .Items import ChecksFinderItem, item_table +from .Locations import ChecksFinderLocation, advancement_table +from Options import PerGameCommonOptions from .Rules import set_rules, set_completion_rules -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld client_version = 7 @@ -25,38 +25,34 @@ class ChecksFinderWorld(World): ChecksFinder is a game where you avoid mines and find checks inside the board with the mines! You win when you get all your items and beat the board! """ - game: str = "ChecksFinder" - option_definitions = checksfinder_options - topology_present = True + game = "ChecksFinder" + options_dataclass = PerGameCommonOptions web = ChecksFinderWeb() item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - def _get_checksfinder_data(self): - return { - 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), - 'seed_name': self.multiworld.seed_name, - 'player_name': self.multiworld.get_player_name(self.player), - 'player_id': self.player, - 'client_version': client_version, - 'race': self.multiworld.is_race, - } + def create_regions(self): + menu = Region("Menu", self.player, self.multiworld) + board = Region("Board", self.player, self.multiworld) + board.locations += [ChecksFinderLocation(self.player, loc_name, loc_data.id, board) + for loc_name, loc_data in advancement_table.items()] - def create_items(self): + connection = Entrance(self.player, "New Board", menu) + menu.exits.append(connection) + connection.connect(board) + self.multiworld.regions += [menu, board] + def create_items(self): # Generate item pool itempool = [] - # Add all required progression items - for (name, num) in required_items.items(): - itempool += [name] * num # Add the map width and height stuff - itempool += ["Map Width"] * (10-5) - itempool += ["Map Height"] * (10-5) + itempool += ["Map Width"] * 5 # 10 - 5 + itempool += ["Map Height"] * 5 # 10 - 5 # Add the map bombs - itempool += ["Map Bombs"] * (20-5) + itempool += ["Map Bombs"] * 15 # 20 - 5 # Convert itempool into real items - itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + itempool = [self.create_item(item) for item in itempool] self.multiworld.itempool += itempool @@ -64,28 +60,16 @@ def set_rules(self): set_rules(self.multiworld, self.player) set_completion_rules(self.multiworld, self.player) - def create_regions(self): - menu = Region("Menu", self.player, self.multiworld) - board = Region("Board", self.player, self.multiworld) - board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) - for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] - - connection = Entrance(self.player, "New Board", menu) - menu.exits.append(connection) - connection.connect(board) - self.multiworld.regions += [menu, board] - def fill_slot_data(self): - slot_data = self._get_checksfinder_data() - for option_name in checksfinder_options: - option = getattr(self.multiworld, option_name)[self.player] - if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: - slot_data[option_name] = int(option.value) - return slot_data + return { + "world_seed": self.random.getrandbits(32), + "seed_name": self.multiworld.seed_name, + "player_name": self.player_name, + "player_id": self.player, + "client_version": client_version, + "race": self.multiworld.is_race, + } - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> ChecksFinderItem: item_data = item_table[name] - item = ChecksFinderItem(name, - ItemClassification.progression if item_data.progression else ItemClassification.filler, - item_data.code, self.player) - return item + return ChecksFinderItem(name, ItemClassification.progression, item_data.code, self.player) diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index c9569376c5f6..cb33ab39591a 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -24,8 +24,3 @@ next to an icon, the number is how many you have gotten and the icon represents Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. -## Unique Local Commands - -The following command is only available when using the ChecksFinderClient to play with Archipelago. - -- `/resync` Manually trigger a resync. diff --git a/worlds/checksfinder/docs/setup_en.md b/worlds/checksfinder/docs/setup_en.md index 673b34900af7..e15763ab3110 100644 --- a/worlds/checksfinder/docs/setup_en.md +++ b/worlds/checksfinder/docs/setup_en.md @@ -4,7 +4,6 @@ - ChecksFinder from the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version) -- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) ## Configuring your YAML file @@ -17,28 +16,15 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options) -### Generating a ChecksFinder game +## Joining a MultiWorld Game -**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if -you play it by itself with another person!** - -When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, -the host will provide you with either a link to download your data file, or with a zip file containing everyone's data -files. You do not have a file inside that zip though! - -You need to start ChecksFinder client yourself, it is located within the Archipelago folder. - -### Connect to the MultiServer - -First start ChecksFinder. - -Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the -`Ip Address` and `Port` separated with a `:` symbol. - -The client will then ask for the username you chose, input that in the text box at the bottom of the client. - -### Play the game - -When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a -multiworld game! +1. Start ChecksFinder +2. Enter the following information: + - Enter the server url (starting from `wss://` for https connection like archipelago.gg, and starting from `ws://` for http connection and local multiserver) + - Enter server port + - Enter the name of the slot you wish to connect to + - Enter the room password (optional) + - Press `Play Online` to connect +3. Start playing! +Game options and controls are described in the readme on the github repository for the game From 8ddb49f0710244945bc9b378368f691e0810fc15 Mon Sep 17 00:00:00 2001 From: digiholic Date: Tue, 6 Aug 2024 15:13:11 -0600 Subject: [PATCH 119/393] OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/osrs/Items.py | 85 +++ worlds/osrs/Locations.py | 21 + worlds/osrs/LogicCSV/LogicCSVToPython.py | 144 +++++ worlds/osrs/LogicCSV/items_generated.py | 43 ++ worlds/osrs/LogicCSV/locations_generated.py | 127 ++++ worlds/osrs/LogicCSV/regions_generated.py | 47 ++ worlds/osrs/LogicCSV/resources_generated.py | 54 ++ worlds/osrs/Names.py | 212 +++++++ worlds/osrs/Options.py | 474 ++++++++++++++ worlds/osrs/Regions.py | 12 + worlds/osrs/__init__.py | 657 ++++++++++++++++++++ worlds/osrs/docs/en_Old School Runescape.md | 114 ++++ worlds/osrs/docs/setup_en.md | 58 ++ 15 files changed, 2052 insertions(+) create mode 100644 worlds/osrs/Items.py create mode 100644 worlds/osrs/Locations.py create mode 100644 worlds/osrs/LogicCSV/LogicCSVToPython.py create mode 100644 worlds/osrs/LogicCSV/items_generated.py create mode 100644 worlds/osrs/LogicCSV/locations_generated.py create mode 100644 worlds/osrs/LogicCSV/regions_generated.py create mode 100644 worlds/osrs/LogicCSV/resources_generated.py create mode 100644 worlds/osrs/Names.py create mode 100644 worlds/osrs/Options.py create mode 100644 worlds/osrs/Regions.py create mode 100644 worlds/osrs/__init__.py create mode 100644 worlds/osrs/docs/en_Old School Runescape.md create mode 100644 worlds/osrs/docs/setup_en.md diff --git a/README.md b/README.md index cebd4f7e7529..5b66e3db8782 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Currently, the following games are supported: * Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 * A Hat in Time +* Old School Runescape For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index ab841e65ee4c..4f012c306be9 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -115,6 +115,9 @@ # Ocarina of Time /worlds/oot/ @espeon65536 +# Old School Runescape +/worlds/osrs @digiholic + # Overcooked! 2 /worlds/overcooked2/ @toasterparty diff --git a/worlds/osrs/Items.py b/worlds/osrs/Items.py new file mode 100644 index 000000000000..0679c964e772 --- /dev/null +++ b/worlds/osrs/Items.py @@ -0,0 +1,85 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemNames + + +class ItemRow(typing.NamedTuple): + name: str + amount: int + progression: ItemClassification + + +class OSRSItem(Item): + game: str = "Old School Runescape" + + +QP_Items: typing.List[str] = [ + ItemNames.QP_Cooks_Assistant, + ItemNames.QP_Demon_Slayer, + ItemNames.QP_Restless_Ghost, + ItemNames.QP_Romeo_Juliet, + ItemNames.QP_Sheep_Shearer, + ItemNames.QP_Shield_of_Arrav, + ItemNames.QP_Ernest_the_Chicken, + ItemNames.QP_Vampyre_Slayer, + ItemNames.QP_Imp_Catcher, + ItemNames.QP_Prince_Ali_Rescue, + ItemNames.QP_Dorics_Quest, + ItemNames.QP_Black_Knights_Fortress, + ItemNames.QP_Witchs_Potion, + ItemNames.QP_Knights_Sword, + ItemNames.QP_Goblin_Diplomacy, + ItemNames.QP_Pirates_Treasure, + ItemNames.QP_Rune_Mysteries, + ItemNames.QP_Misthalin_Mystery, + ItemNames.QP_Corsair_Curse, + ItemNames.QP_X_Marks_the_Spot, + ItemNames.QP_Below_Ice_Mountain +] + +starting_area_dict: typing.Dict[int, str] = { + 0: ItemNames.Lumbridge, + 1: ItemNames.Al_Kharid, + 2: ItemNames.Central_Varrock, + 3: ItemNames.West_Varrock, + 4: ItemNames.Edgeville, + 5: ItemNames.Falador, + 6: ItemNames.Draynor_Village, + 7: ItemNames.Wilderness, +} + +chunksanity_starting_chunks: typing.List[str] = [ + ItemNames.Lumbridge, + ItemNames.Lumbridge_Swamp, + ItemNames.Lumbridge_Farms, + ItemNames.HAM_Hideout, + ItemNames.Draynor_Village, + ItemNames.Draynor_Manor, + ItemNames.Wizards_Tower, + ItemNames.Al_Kharid, + ItemNames.Citharede_Abbey, + ItemNames.South_Of_Varrock, + ItemNames.Central_Varrock, + ItemNames.Varrock_Palace, + ItemNames.East_Of_Varrock, + ItemNames.West_Varrock, + ItemNames.Edgeville, + ItemNames.Barbarian_Village, + ItemNames.Monastery, + ItemNames.Ice_Mountain, + ItemNames.Dwarven_Mines, + ItemNames.Falador, + ItemNames.Falador_Farm, + ItemNames.Crafting_Guild, + ItemNames.Rimmington, + ItemNames.Port_Sarim, + ItemNames.Mudskipper_Point, + ItemNames.Wilderness +] + +# Some starting areas contain multiple regions, so if that area is rolled for Chunksanity, we need to map it to one +chunksanity_special_region_names: typing.Dict[str, str] = { + ItemNames.Lumbridge_Farms: 'Lumbridge Farms East', + ItemNames.Crafting_Guild: 'Crafting Guild Outskirts', +} diff --git a/worlds/osrs/Locations.py b/worlds/osrs/Locations.py new file mode 100644 index 000000000000..b5827d60f2fe --- /dev/null +++ b/worlds/osrs/Locations.py @@ -0,0 +1,21 @@ +import typing + +from BaseClasses import Location + + +class SkillRequirement(typing.NamedTuple): + skill: str + level: int + + +class LocationRow(typing.NamedTuple): + name: str + category: str + regions: typing.List[str] + skills: typing.List[SkillRequirement] + items: typing.List[str] + qp: int + + +class OSRSLocation(Location): + game: str = "Old School Runescape" diff --git a/worlds/osrs/LogicCSV/LogicCSVToPython.py b/worlds/osrs/LogicCSV/LogicCSVToPython.py new file mode 100644 index 000000000000..ed8bd8172a01 --- /dev/null +++ b/worlds/osrs/LogicCSV/LogicCSVToPython.py @@ -0,0 +1,144 @@ +""" +This is a utility file that converts logic in the form of CSV files into Python files that can be imported and used +directly by the world implementation. Whenever the logic files are updated, this script should be run to re-generate +the python files containing the data. +""" +import requests + +# The CSVs are updated at this repository to be shared between generator and client. +data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/" +# The Github tag of the CSVs this was generated with +data_csv_tag = "v1.5" + +if __name__ == "__main__": + import sys + import os + import csv + import typing + + # makes this module runnable from its world folder. Shamelessly stolen from Subnautica + sys.path.remove(os.path.dirname(__file__)) + 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) + + + def load_location_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile: + locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n") + locPyFile.write("\n") + locPyFile.write("location_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req: + locations_reader = csv.reader(req.text.splitlines()) + for row in locations_reader: + row_line = "LocationRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1].lower()) + + region_strings = row[2].split(", ") if row[2] else [] + row_line += f"{str_list_to_py(region_strings)}, " + + skill_strings = row[3].split(", ") + row_line += "[" + if skill_strings: + split_skills = [skill.split(" ") for skill in skill_strings if skill != ""] + if split_skills: + for split in split_skills: + row_line += f"SkillRequirement('{split[0]}', {split[1]}), " + row_line += "], " + + item_strings = row[4].split(", ") if row[4] else [] + row_line += f"{str_list_to_py(item_strings)}, " + row_line += f"{row[5]})" if row[5] != "" else "0)" + locPyFile.write(f"\t{row_line},\n") + locPyFile.write("]\n") + + def load_region_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile: + regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + regPyFile.write("from ..Regions import RegionRow\n") + regPyFile.write("\n") + regPyFile.write("region_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req: + regions_reader = csv.reader(req.text.splitlines()) + for row in regions_reader: + row_line = "RegionRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1]) + connections = row[2].replace("'", "\\'") + row_line += f"{str_list_to_py(connections.split(', '))}, " + resources = row[3].replace("'", "\\'") + row_line += f"{str_list_to_py(resources.split(', '))})" + regPyFile.write(f"\t{row_line},\n") + regPyFile.write("]\n") + + def load_resource_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile: + resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + resPyFile.write("from ..Regions import ResourceRow\n") + resPyFile.write("\n") + resPyFile.write("resource_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req: + resource_reader = csv.reader(req.text.splitlines()) + for row in resource_reader: + name = row[0].replace("'", "\\'") + row_line = f"ResourceRow('{name}')" + resPyFile.write(f"\t{row_line},\n") + resPyFile.write("]\n") + + + def load_item_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile: + itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + itemPyfile.write("from BaseClasses import ItemClassification\n") + itemPyfile.write("from ..Items import ItemRow\n") + itemPyfile.write("\n") + itemPyfile.write("item_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req: + item_reader = csv.reader(req.text.splitlines()) + for row in item_reader: + row_line = "ItemRow(" + row_line += str_format(row[0]) + row_line += f"{row[1]}, " + + row_line += f"ItemClassification.{row[2]})" + + itemPyfile.write(f"\t{row_line},\n") + itemPyfile.write("]\n") + + + def str_format(s) -> str: + ret_str = s.replace("'", "\\'") + return f"'{ret_str}', " + + + def str_list_to_py(str_list) -> str: + ret_str = "[" + for s in str_list: + ret_str += f"'{s}', " + ret_str += "]" + return ret_str + + + + load_location_csv() + print("Generated locations py") + load_region_csv() + print("Generated regions py") + load_resource_csv() + print("Generated resource py") + load_item_csv() + print("Generated item py") diff --git a/worlds/osrs/LogicCSV/items_generated.py b/worlds/osrs/LogicCSV/items_generated.py new file mode 100644 index 000000000000..b5e610a6e3ab --- /dev/null +++ b/worlds/osrs/LogicCSV/items_generated.py @@ -0,0 +1,43 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from BaseClasses import ItemClassification +from ..Items import ItemRow + +item_rows = [ + ItemRow('Area: Lumbridge', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Swamp', 1, ItemClassification.progression), + ItemRow('Area: HAM Hideout', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression), + ItemRow('Area: South of Varrock', 1, ItemClassification.progression), + ItemRow('Area: East Varrock', 1, ItemClassification.progression), + ItemRow('Area: Central Varrock', 1, ItemClassification.progression), + ItemRow('Area: Varrock Palace', 1, ItemClassification.progression), + ItemRow('Area: West Varrock', 1, ItemClassification.progression), + ItemRow('Area: Edgeville', 1, ItemClassification.progression), + ItemRow('Area: Barbarian Village', 1, ItemClassification.progression), + ItemRow('Area: Draynor Manor', 1, ItemClassification.progression), + ItemRow('Area: Falador', 1, ItemClassification.progression), + ItemRow('Area: Dwarven Mines', 1, ItemClassification.progression), + ItemRow('Area: Ice Mountain', 1, ItemClassification.progression), + ItemRow('Area: Monastery', 1, ItemClassification.progression), + ItemRow('Area: Falador Farms', 1, ItemClassification.progression), + ItemRow('Area: Port Sarim', 1, ItemClassification.progression), + ItemRow('Area: Mudskipper Point', 1, ItemClassification.progression), + ItemRow('Area: Karamja', 1, ItemClassification.progression), + ItemRow('Area: Crandor', 1, ItemClassification.progression), + ItemRow('Area: Rimmington', 1, ItemClassification.progression), + ItemRow('Area: Crafting Guild', 1, ItemClassification.progression), + ItemRow('Area: Draynor Village', 1, ItemClassification.progression), + ItemRow('Area: Wizard Tower', 1, ItemClassification.progression), + ItemRow('Area: Corsair Cove', 1, ItemClassification.progression), + ItemRow('Area: Al Kharid', 1, ItemClassification.progression), + ItemRow('Area: Citharede Abbey', 1, ItemClassification.progression), + ItemRow('Area: Wilderness', 1, ItemClassification.progression), + ItemRow('Progressive Armor', 6, ItemClassification.progression), + ItemRow('Progressive Weapons', 6, ItemClassification.progression), + ItemRow('Progressive Tools', 6, ItemClassification.useful), + ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful), + ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful), + ItemRow('Progressive Magic', 2, ItemClassification.useful), +] diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py new file mode 100644 index 000000000000..073e505ad8f4 --- /dev/null +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -0,0 +1,127 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Locations import LocationRow, SkillRequirement + +location_rows = [ + LocationRow('Quest: Cook\'s Assistant', 'quest', ['Lumbridge', 'Wheat', 'Windmill', 'Egg', 'Milk', ], [], [], 0), + LocationRow('Quest: Demon Slayer', 'quest', ['Central Varrock', 'Varrock Palace', 'Wizard Tower', 'South of Varrock', ], [], [], 0), + LocationRow('Quest: The Restless Ghost', 'quest', ['Lumbridge', 'Lumbridge Swamp', 'Wizard Tower', ], [], [], 0), + LocationRow('Quest: Romeo & Juliet', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Sheep Shearer', 'quest', ['Lumbridge Farms West', 'Spinning Wheel', ], [], [], 0), + LocationRow('Quest: Shield of Arrav', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Ernest the Chicken', 'quest', ['Draynor Manor', ], [], [], 0), + LocationRow('Quest: Vampyre Slayer', 'quest', ['Draynor Village', 'Central Varrock', 'Draynor Manor', ], [], [], 0), + LocationRow('Quest: Imp Catcher', 'quest', ['Wizard Tower', 'Imps', ], [], [], 0), + LocationRow('Quest: Prince Ali Rescue', 'quest', ['Al Kharid', 'Central Varrock', 'Bronze Ores', 'Clay Ore', 'Sheep', 'Spinning Wheel', 'Draynor Village', ], [], [], 0), + LocationRow('Quest: Doric\'s Quest', 'quest', ['Dwarven Mountain Pass', 'Clay Ore', 'Iron Ore', 'Bronze Ores', ], [SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Quest: Black Knights\' Fortress', 'quest', ['Dwarven Mines', 'Falador', 'Monastery', 'Ice Mountain', 'Falador Farms', ], [], ['Progressive Armor', ], 12), + LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0), + LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0), + LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0), + LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0), + LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0), + LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16), + LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32), + LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0), + LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2), + LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6), + LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0), + LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0), + LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2), + LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6), + LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0), + LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0), + LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0), + LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0), + LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0), + LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0), + LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4), + LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8), + LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0), + LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0), + LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0), + LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2), + LocationRow('Catch a Lobster', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 40), ], [], 6), + LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), + LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), + LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), + LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), + LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), + LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0), + LocationRow('Kill a Barbarian', 'combat', ['Barbarian', ], [SkillRequirement('Combat', 10), ], [], 0), + LocationRow('Kill a Giant Frog', 'combat', ['Lumbridge Swamp', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Zombie', 'combat', ['Zombie', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Guard', 'combat', ['Guard', ], [SkillRequirement('Combat', 21), ], [], 0), + LocationRow('Kill a Hill Giant', 'combat', ['Hill Giant', ], [SkillRequirement('Combat', 28), ], [], 2), + LocationRow('Kill a Deadly Red Spider', 'combat', ['Deadly Red Spider', ], [SkillRequirement('Combat', 34), ], [], 2), + LocationRow('Kill a Moss Giant', 'combat', ['Moss Giant', ], [SkillRequirement('Combat', 42), ], [], 2), + LocationRow('Kill a Catablepon', 'combat', ['Barbarian Village', ], [SkillRequirement('Combat', 49), ], [], 4), + LocationRow('Kill an Ice Giant', 'combat', ['Ice Giant', ], [SkillRequirement('Combat', 53), ], [], 4), + LocationRow('Kill a Lesser Demon', 'combat', ['Lesser Demon', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28), + LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28), + LocationRow('Total XP 5,000', 'general', [], [], [], 0), + LocationRow('Combat Level 5', 'general', [], [], [], 0), + LocationRow('Total XP 10,000', 'general', [], [], [], 0), + LocationRow('Total Level 50', 'general', [], [], [], 0), + LocationRow('Total XP 25,000', 'general', [], [], [], 0), + LocationRow('Total Level 100', 'general', [], [], [], 0), + LocationRow('Total XP 50,000', 'general', [], [], [], 0), + LocationRow('Combat Level 15', 'general', [], [], [], 0), + LocationRow('Total Level 150', 'general', [], [], [], 2), + LocationRow('Total XP 75,000', 'general', [], [], [], 2), + LocationRow('Combat Level 25', 'general', [], [], [], 2), + LocationRow('Total XP 100,000', 'general', [], [], [], 6), + LocationRow('Total Level 200', 'general', [], [], [], 6), + LocationRow('Total XP 125,000', 'general', [], [], [], 6), + LocationRow('Combat Level 30', 'general', [], [], [], 10), + LocationRow('Total Level 250', 'general', [], [], [], 10), + LocationRow('Total XP 150,000', 'general', [], [], [], 10), + LocationRow('Total Level 300', 'general', [], [], [], 16), + LocationRow('Combat Level 40', 'general', [], [], [], 16), + LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0), + LocationRow('Points: Demon Slayer', 'points', [], [], [], 0), + LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0), + LocationRow('Points: Romeo & Juliet', 'points', [], [], [], 0), + LocationRow('Points: Sheep Shearer', 'points', [], [], [], 0), + LocationRow('Points: Shield of Arrav', 'points', [], [], [], 0), + LocationRow('Points: Ernest the Chicken', 'points', [], [], [], 0), + LocationRow('Points: Vampyre Slayer', 'points', [], [], [], 0), + LocationRow('Points: Imp Catcher', 'points', [], [], [], 0), + LocationRow('Points: Prince Ali Rescue', 'points', [], [], [], 0), + LocationRow('Points: Doric\'s Quest', 'points', [], [], [], 0), + LocationRow('Points: Black Knights\' Fortress', 'points', [], [], [], 0), + LocationRow('Points: Witch\'s Potion', 'points', [], [], [], 0), + LocationRow('Points: The Knight\'s Sword', 'points', [], [], [], 0), + LocationRow('Points: Goblin Diplomacy', 'points', [], [], [], 0), + LocationRow('Points: Pirate\'s Treasure', 'points', [], [], [], 0), + LocationRow('Points: Rune Mysteries', 'points', [], [], [], 0), + LocationRow('Points: Misthalin Mystery', 'points', [], [], [], 0), + LocationRow('Points: The Corsair Curse', 'points', [], [], [], 0), + LocationRow('Points: X Marks the Spot', 'points', [], [], [], 0), + LocationRow('Points: Below Ice Mountain', 'points', [], [], [], 0), +] diff --git a/worlds/osrs/LogicCSV/regions_generated.py b/worlds/osrs/LogicCSV/regions_generated.py new file mode 100644 index 000000000000..87b3747d938e --- /dev/null +++ b/worlds/osrs/LogicCSV/regions_generated.py @@ -0,0 +1,47 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import RegionRow + +region_rows = [ + RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]), + RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]), + RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]), + RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]), + RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]), + RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]), + RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]), + RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]), + RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]), + RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]), + RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]), + RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]), + RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]), + RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]), + RegionRow('Falador East Outskirts', 'Area: Falador', ['Dwarven Mountain Pass', 'Draynor Manor Outskirts', 'Falador Farms', ], ['', ]), + RegionRow('Dwarven Mountain Pass', 'Area: Dwarven Mines', ['Goblin Village', 'Monastery', 'Barbarian Village', 'Falador East Outskirts', 'Falador', ], ['Anvil*', 'Wheat', ]), + RegionRow('Dwarven Mines', 'Area: Dwarven Mines', ['Monastery', 'Ice Mountain', 'Falador', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Gold Ore', 'Anvil', 'Pie Dish', 'Clay Ore', ]), + RegionRow('Goblin Village', 'Area: Ice Mountain', ['Wilderness', 'Dwarven Mountain Pass', ], ['Meat', ]), + RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]), + RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]), + RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]), + RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]), + RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]), + RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]), + RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]), + RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]), + RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), + RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]), + RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]), + RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]), + RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]), + RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]), + RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]), + RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]), + RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), +] diff --git a/worlds/osrs/LogicCSV/resources_generated.py b/worlds/osrs/LogicCSV/resources_generated.py new file mode 100644 index 000000000000..18c2ebe2f317 --- /dev/null +++ b/worlds/osrs/LogicCSV/resources_generated.py @@ -0,0 +1,54 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import ResourceRow + +resource_rows = [ + ResourceRow('Mind Runes'), + ResourceRow('Spinning Wheel'), + ResourceRow('Sheep'), + ResourceRow('Furnace'), + ResourceRow('Chisel'), + ResourceRow('Bronze Ores'), + ResourceRow('Iron Ore'), + ResourceRow('Silver Ore'), + ResourceRow('Coal Ore'), + ResourceRow('Gold Ore'), + ResourceRow('Bronze Anvil'), + ResourceRow('Anvil'), + ResourceRow('Shrimp Spot'), + ResourceRow('Fly Fishing Spot'), + ResourceRow('Lobster Spot'), + ResourceRow('Redberry Bush'), + ResourceRow('Bowl'), + ResourceRow('Meat'), + ResourceRow('Cooking Apple'), + ResourceRow('Pie Dish'), + ResourceRow('Cake Tin'), + ResourceRow('Wheat'), + ResourceRow('Windmill'), + ResourceRow('Egg'), + ResourceRow('Milk'), + ResourceRow('Cheese'), + ResourceRow('Tomato'), + ResourceRow('Oak Tree'), + ResourceRow('Willow Tree'), + ResourceRow('Canoe Tree'), + ResourceRow('Goblin'), + ResourceRow('Barbarian'), + ResourceRow('Zombie'), + ResourceRow('Guard'), + ResourceRow('Hill Giant'), + ResourceRow('Deadly Red Spider'), + ResourceRow('Moss Giant'), + ResourceRow('Ice Giant'), + ResourceRow('Lesser Demon'), + ResourceRow('Rune Essence'), + ResourceRow('Crafting Moulds'), + ResourceRow('Nature Runes'), + ResourceRow('Law Runes'), + ResourceRow('Imps'), + ResourceRow('Clay Ore'), + ResourceRow('Onion'), + ResourceRow('Potato'), +] diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py new file mode 100644 index 000000000000..95aed742b6f1 --- /dev/null +++ b/worlds/osrs/Names.py @@ -0,0 +1,212 @@ +from enum import Enum + + +class RegionNames(str, Enum): + Lumbridge = "Lumbridge" + Lumbridge_Swamp = "Lumbridge Swamp" + Lumbridge_Farms_East = "Lumbridge Farms East" + Lumbridge_Farms_West = "Lumbridge Farms West" + HAM_Hideout = "HAM Hideout" + Draynor_Village = "Draynor Village" + Draynor_Manor = "Draynor Manor" + Wizards_Tower = "Wizard Tower" + Al_Kharid = "Al Kharid" + Citharede_Abbey = "Citharede Abbey" + South_Of_Varrock = "South of Varrock" + Central_Varrock = "Central Varrock" + Varrock_Palace = "Varrock Palace" + East_Of_Varrock = "East Varrock" + West_Varrock = "West Varrock" + Edgeville = "Edgeville" + Barbarian_Village = "Barbarian Village" + Monastery = "Monastery" + Ice_Mountain = "Ice Mountain" + Dwarven_Mines = "Dwarven Mines" + Falador = "Falador" + Falador_Farm = "Falador Farms" + Crafting_Guild = "Crafting Guild" + Cooks_Guild = "Cook's Guild" + Rimmington = "Rimmington" + Port_Sarim = "Port Sarim" + Mudskipper_Point = "Mudskipper Point" + Karamja = "Karamja" + Corsair_Cove = "Corsair Cove" + Wilderness = "The Wilderness" + Crandor = "Crandor" + # Resource Regions + Egg = "Egg" + Sheep = "Sheep" + Milk = "Milk" + Wheat = "Wheat" + Windmill = "Windmill" + Spinning_Wheel = "Spinning Wheel" + Imp = "Imp" + Bronze_Ores = "Bronze Ores" + Clay_Rock = "Clay Ore" + Coal_Rock = "Coal Ore" + Iron_Rock = "Iron Ore" + Silver_Rock = "Silver Ore" + Gold_Rock = "Gold Ore" + Furnace = "Furnace" + Anvil = "Anvil" + Oak_Tree = "Oak Tree" + Willow_Tree = "Willow Tree" + Shrimp = "Shrimp Spot" + Fly_Fish = "Fly Fishing Spot" + Lobster = "Lobster Spot" + Mind_Runes = "Mind Runes" + Canoe_Tree = "Canoe Tree" + + __str__ = str.__str__ + + +class ItemNames(str, Enum): + Lumbridge = "Area: Lumbridge" + Lumbridge_Swamp = "Area: Lumbridge Swamp" + Lumbridge_Farms = "Area: Lumbridge Farms" + HAM_Hideout = "Area: HAM Hideout" + Draynor_Village = "Area: Draynor Village" + Draynor_Manor = "Area: Draynor Manor" + Wizards_Tower = "Area: Wizard Tower" + Al_Kharid = "Area: Al Kharid" + Citharede_Abbey = "Area: Citharede Abbey" + South_Of_Varrock = "Area: South of Varrock" + Central_Varrock = "Area: Central Varrock" + Varrock_Palace = "Area: Varrock Palace" + East_Of_Varrock = "Area: East Varrock" + West_Varrock = "Area: West Varrock" + Edgeville = "Area: Edgeville" + Barbarian_Village = "Area: Barbarian Village" + Monastery = "Area: Monastery" + Ice_Mountain = "Area: Ice Mountain" + Dwarven_Mines = "Area: Dwarven Mines" + Falador = "Area: Falador" + Falador_Farm = "Area: Falador Farms" + Crafting_Guild = "Area: Crafting Guild" + Rimmington = "Area: Rimmington" + Port_Sarim = "Area: Port Sarim" + Mudskipper_Point = "Area: Mudskipper Point" + Karamja = "Area: Karamja" + Crandor = "Area: Crandor" + Corsair_Cove = "Area: Corsair Cove" + Wilderness = "Area: Wilderness" + Progressive_Armor = "Progressive Armor" + Progressive_Weapons = "Progressive Weapons" + Progressive_Tools = "Progressive Tools" + Progressive_Range_Armor = "Progressive Range Armor" + Progressive_Range_Weapon = "Progressive Range Weapon" + Progressive_Magic = "Progressive Magic Spell" + Lobsters = "10 Lobsters" + Swordfish = "5 Swordfish" + Energy_Potions = "10 Energy Potions" + Coins = "5,000 Coins" + Mind_Runes = "50 Mind Runes" + Chaos_Runes = "25 Chaos Runes" + Death_Runes = "10 Death Runes" + Law_Runes = "10 Law Runes" + QP_Cooks_Assistant = "1 QP (Cook's Assistant)" + QP_Demon_Slayer = "3 QP (Demon Slayer)" + QP_Restless_Ghost = "1 QP (The Restless Ghost)" + QP_Romeo_Juliet = "5 QP (Romeo & Juliet)" + QP_Sheep_Shearer = "1 QP (Sheep Shearer)" + QP_Shield_of_Arrav = "1 QP (Shield of Arrav)" + QP_Ernest_the_Chicken = "4 QP (Ernest the Chicken)" + QP_Vampyre_Slayer = "3 QP (Vampyre Slayer)" + QP_Imp_Catcher = "1 QP (Imp Catcher)" + QP_Prince_Ali_Rescue = "3 QP (Prince Ali Rescue)" + QP_Dorics_Quest = "1 QP (Doric's Quest)" + QP_Black_Knights_Fortress = "3 QP (Black Knights' Fortress)" + QP_Witchs_Potion = "1 QP (Witch's Potion)" + QP_Knights_Sword = "1 QP (The Knight's Sword)" + QP_Goblin_Diplomacy = "5 QP (Goblin Diplomacy)" + QP_Pirates_Treasure = "2 QP (Pirate's Treasure)" + QP_Rune_Mysteries = "1 QP (Rune Mysteries)" + QP_Misthalin_Mystery = "1 QP (Misthalin Mystery)" + QP_Corsair_Curse = "2 QP (The Corsair Curse)" + QP_X_Marks_the_Spot = "1 QP (X Marks The Spot)" + QP_Below_Ice_Mountain = "1 QP (Below Ice Mountain)" + + __str__ = str.__str__ + + +class LocationNames(str, Enum): + Q_Cooks_Assistant = "Quest: Cook's Assistant" + Q_Demon_Slayer = "Quest: Demon Slayer" + Q_Restless_Ghost = "Quest: The Restless Ghost" + Q_Romeo_Juliet = "Quest: Romeo & Juliet" + Q_Sheep_Shearer = "Quest: Sheep Shearer" + Q_Shield_of_Arrav = "Quest: Shield of Arrav" + Q_Ernest_the_Chicken = "Quest: Ernest the Chicken" + Q_Vampyre_Slayer = "Quest: Vampyre Slayer" + Q_Imp_Catcher = "Quest: Imp Catcher" + Q_Prince_Ali_Rescue = "Quest: Prince Ali Rescue" + Q_Dorics_Quest = "Quest: Doric's Quest" + Q_Black_Knights_Fortress = "Quest: Black Knights' Fortress" + Q_Witchs_Potion = "Quest: Witch's Potion" + Q_Knights_Sword = "Quest: The Knight's Sword" + Q_Goblin_Diplomacy = "Quest: Goblin Diplomacy" + Q_Pirates_Treasure = "Quest: Pirate's Treasure" + Q_Rune_Mysteries = "Quest: Rune Mysteries" + Q_Misthalin_Mystery = "Quest: Misthalin Mystery" + Q_Corsair_Curse = "Quest: The Corsair Curse" + Q_X_Marks_the_Spot = "Quest: X Marks the Spot" + Q_Below_Ice_Mountain = "Quest: Below Ice Mountain" + QP_Cooks_Assistant = "Points: Cook's Assistant" + QP_Demon_Slayer = "Points: Demon Slayer" + QP_Restless_Ghost = "Points: The Restless Ghost" + QP_Romeo_Juliet = "Points: Romeo & Juliet" + QP_Sheep_Shearer = "Points: Sheep Shearer" + QP_Shield_of_Arrav = "Points: Shield of Arrav" + QP_Ernest_the_Chicken = "Points: Ernest the Chicken" + QP_Vampyre_Slayer = "Points: Vampyre Slayer" + QP_Imp_Catcher = "Points: Imp Catcher" + QP_Prince_Ali_Rescue = "Points: Prince Ali Rescue" + QP_Dorics_Quest = "Points: Doric's Quest" + QP_Black_Knights_Fortress = "Points: Black Knights' Fortress" + QP_Witchs_Potion = "Points: Witch's Potion" + QP_Knights_Sword = "Points: The Knight's Sword" + QP_Goblin_Diplomacy = "Points: Goblin Diplomacy" + QP_Pirates_Treasure = "Points: Pirate's Treasure" + QP_Rune_Mysteries = "Points: Rune Mysteries" + QP_Misthalin_Mystery = "Points: Misthalin Mystery" + QP_Corsair_Curse = "Points: The Corsair Curse" + QP_X_Marks_the_Spot = "Points: X Marks the Spot" + QP_Below_Ice_Mountain = "Points: Below Ice Mountain" + Guppy = "Prepare a Guppy" + Cavefish = "Prepare a Cavefish" + Tetra = "Prepare a Tetra" + Barronite_Deposit = "Crush a Barronite Deposit" + Oak_Log = "Cut an Oak Log" + Willow_Log = "Cut a Willow Log" + Catch_Lobster = "Catch a Lobster" + Mine_Silver = "Mine Silver" + Mine_Coal = "Mine Coal" + Mine_Gold = "Mine Gold" + Smelt_Silver = "Smelt a Silver Bar" + Smelt_Steel = "Smelt a Steel Bar" + Smelt_Gold = "Smelt a Gold Bar" + Cut_Sapphire = "Cut a Sapphire" + Cut_Emerald = "Cut an Emerald" + Cut_Ruby = "Cut a Ruby" + Cut_Diamond = "Cut a Diamond" + K_Lesser_Demon = "Kill a Lesser Demon" + K_Ogress_Shaman = "Kill an Ogress Shaman" + Bake_Apple_Pie = "Bake an Apple Pie" + Bake_Cake = "Bake a Cake" + Bake_Meat_Pizza = "Bake a Meat Pizza" + Total_XP_5000 = "5,000 Total XP" + Total_XP_10000 = "10,000 Total XP" + Total_XP_25000 = "25,000 Total XP" + Total_XP_50000 = "50,000 Total XP" + Total_XP_100000 = "100,000 Total XP" + Total_Level_50 = "Total Level 50" + Total_Level_100 = "Total Level 100" + Total_Level_150 = "Total Level 150" + Total_Level_200 = "Total Level 200" + Combat_Level_5 = "Combat Level 5" + Combat_Level_15 = "Combat Level 15" + Combat_Level_25 = "Combat Level 25" + Travel_on_a_Canoe = "Travel on a Canoe" + Q_Dragon_Slayer = "Quest: Dragon Slayer" + + __str__ = str.__str__ diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py new file mode 100644 index 000000000000..520cd8e8b06b --- /dev/null +++ b/worlds/osrs/Options.py @@ -0,0 +1,474 @@ +from dataclasses import dataclass + +from Options import Choice, Toggle, Range, PerGameCommonOptions + +MAX_COMBAT_TASKS = 16 +MAX_PRAYER_TASKS = 3 +MAX_MAGIC_TASKS = 4 +MAX_RUNECRAFT_TASKS = 3 +MAX_CRAFTING_TASKS = 5 +MAX_MINING_TASKS = 5 +MAX_SMITHING_TASKS = 4 +MAX_FISHING_TASKS = 5 +MAX_COOKING_TASKS = 5 +MAX_FIREMAKING_TASKS = 2 +MAX_WOODCUTTING_TASKS = 3 + +NON_QUEST_LOCATION_COUNT = 22 + + +class StartingArea(Choice): + """ + Which chunks are available at the start. The player may need to move through locked chunks to reach the starting + area, but any areas that require quests, skills, or coins are not available as a starting location. + + "Any Bank" rolls a random region that contains a bank. + Chunksanity can start you in any chunk. Hope you like woodcutting! + """ + display_name = "Starting Region" + option_lumbridge = 0 + option_al_kharid = 1 + option_varrock_east = 2 + option_varrock_west = 3 + option_edgeville = 4 + option_falador = 5 + option_draynor = 6 + option_wilderness = 7 + option_any_bank = 8 + option_chunksanity = 9 + default = 0 + + +class BrutalGrinds(Toggle): + """ + Whether to allow skill tasks without having reasonable access to the usual skill training path. + For example, if enabled, you could be forced to train smithing without an anvil purely by smelting bars, + or training fishing to high levels entirely on shrimp. + """ + display_name = "Allow Brutal Grinds" + + +class ProgressiveTasks(Toggle): + """ + Whether skill tasks should always be generated in order of easiest to hardest. + If enabled, you would not be assigned "Mine Gold" without also being assigned + "Mine Silver", "Mine Coal", and "Mine Iron". Enabling this will result in a generally shorter seed, but with + a lower variety of tasks. + """ + display_name = "Progressive Tasks" + + +class MaxCombatLevel(Range): + """ + The highest combat level of monster to possibly be assigned as a task. + If set to 0, no combat tasks will be generated. + """ + range_start = 0 + range_end = 1520 + default = 50 + + +class MaxCombatTasks(Range): + """ + The maximum number of Combat Tasks to possibly be assigned. + If set to 0, no combat tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_COMBAT_TASKS + default = MAX_COMBAT_TASKS + + +class CombatTaskWeight(Range): + """ + How much to favor generating combat tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxPrayerLevel(Range): + """ + The highest Prayer requirement of any task generated. + If set to 0, no Prayer tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxPrayerTasks(Range): + """ + The maximum number of Prayer Tasks to possibly be assigned. + If set to 0, no Prayer tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_PRAYER_TASKS + default = MAX_PRAYER_TASKS + + +class PrayerTaskWeight(Range): + """ + How much to favor generating Prayer tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMagicLevel(Range): + """ + The highest Magic requirement of any task generated. + If set to 0, no Magic tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMagicTasks(Range): + """ + The maximum number of Magic Tasks to possibly be assigned. + If set to 0, no Magic tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_MAGIC_TASKS + default = MAX_MAGIC_TASKS + + +class MagicTaskWeight(Range): + """ + How much to favor generating Magic tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxRunecraftLevel(Range): + """ + The highest Runecraft requirement of any task generated. + If set to 0, no Runecraft tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxRunecraftTasks(Range): + """ + The maximum number of Runecraft Tasks to possibly be assigned. + If set to 0, no Runecraft tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_RUNECRAFT_TASKS + default = MAX_RUNECRAFT_TASKS + + +class RunecraftTaskWeight(Range): + """ + How much to favor generating Runecraft tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCraftingLevel(Range): + """ + The highest Crafting requirement of any task generated. + If set to 0, no Crafting tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCraftingTasks(Range): + """ + The maximum number of Crafting Tasks to possibly be assigned. + If set to 0, no Crafting tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_CRAFTING_TASKS + default = MAX_CRAFTING_TASKS + + +class CraftingTaskWeight(Range): + """ + How much to favor generating Crafting tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMiningLevel(Range): + """ + The highest Mining requirement of any task generated. + If set to 0, no Mining tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMiningTasks(Range): + """ + The maximum number of Mining Tasks to possibly be assigned. + If set to 0, no Mining tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_MINING_TASKS + default = MAX_MINING_TASKS + + +class MiningTaskWeight(Range): + """ + How much to favor generating Mining tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxSmithingLevel(Range): + """ + The highest Smithing requirement of any task generated. + If set to 0, no Smithing tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxSmithingTasks(Range): + """ + The maximum number of Smithing Tasks to possibly be assigned. + If set to 0, no Smithing tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_SMITHING_TASKS + default = MAX_SMITHING_TASKS + + +class SmithingTaskWeight(Range): + """ + How much to favor generating Smithing tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFishingLevel(Range): + """ + The highest Fishing requirement of any task generated. + If set to 0, no Fishing tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFishingTasks(Range): + """ + The maximum number of Fishing Tasks to possibly be assigned. + If set to 0, no Fishing tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_FISHING_TASKS + default = MAX_FISHING_TASKS + + +class FishingTaskWeight(Range): + """ + How much to favor generating Fishing tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCookingLevel(Range): + """ + The highest Cooking requirement of any task generated. + If set to 0, no Cooking tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCookingTasks(Range): + """ + The maximum number of Cooking Tasks to possibly be assigned. + If set to 0, no Cooking tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_COOKING_TASKS + default = MAX_COOKING_TASKS + + +class CookingTaskWeight(Range): + """ + How much to favor generating Cooking tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFiremakingLevel(Range): + """ + The highest Firemaking requirement of any task generated. + If set to 0, no Firemaking tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFiremakingTasks(Range): + """ + The maximum number of Firemaking Tasks to possibly be assigned. + If set to 0, no Firemaking tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_FIREMAKING_TASKS + default = MAX_FIREMAKING_TASKS + + +class FiremakingTaskWeight(Range): + """ + How much to favor generating Firemaking tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxWoodcuttingLevel(Range): + """ + The highest Woodcutting requirement of any task generated. + If set to 0, no Woodcutting tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxWoodcuttingTasks(Range): + """ + The maximum number of Woodcutting Tasks to possibly be assigned. + If set to 0, no Woodcutting tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_WOODCUTTING_TASKS + default = MAX_WOODCUTTING_TASKS + + +class WoodcuttingTaskWeight(Range): + """ + How much to favor generating Woodcutting tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MinimumGeneralTasks(Range): + """ + How many guaranteed general progression tasks to be assigned (total level, total XP, etc.). + General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so + there is no maximum. + """ + range_start = 0 + range_end = NON_QUEST_LOCATION_COUNT + default = 10 + + +class GeneralTaskWeight(Range): + """ + How much to favor generating General tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +@dataclass +class OSRSOptions(PerGameCommonOptions): + starting_area: StartingArea + brutal_grinds: BrutalGrinds + progressive_tasks: ProgressiveTasks + max_combat_level: MaxCombatLevel + max_combat_tasks: MaxCombatTasks + combat_task_weight: CombatTaskWeight + max_prayer_level: MaxPrayerLevel + max_prayer_tasks: MaxPrayerTasks + prayer_task_weight: PrayerTaskWeight + max_magic_level: MaxMagicLevel + max_magic_tasks: MaxMagicTasks + magic_task_weight: MagicTaskWeight + max_runecraft_level: MaxRunecraftLevel + max_runecraft_tasks: MaxRunecraftTasks + runecraft_task_weight: RunecraftTaskWeight + max_crafting_level: MaxCraftingLevel + max_crafting_tasks: MaxCraftingTasks + crafting_task_weight: CraftingTaskWeight + max_mining_level: MaxMiningLevel + max_mining_tasks: MaxMiningTasks + mining_task_weight: MiningTaskWeight + max_smithing_level: MaxSmithingLevel + max_smithing_tasks: MaxSmithingTasks + smithing_task_weight: SmithingTaskWeight + max_fishing_level: MaxFishingLevel + max_fishing_tasks: MaxFishingTasks + fishing_task_weight: FishingTaskWeight + max_cooking_level: MaxCookingLevel + max_cooking_tasks: MaxCookingTasks + cooking_task_weight: CookingTaskWeight + max_firemaking_level: MaxFiremakingLevel + max_firemaking_tasks: MaxFiremakingTasks + firemaking_task_weight: FiremakingTaskWeight + max_woodcutting_level: MaxWoodcuttingLevel + max_woodcutting_tasks: MaxWoodcuttingTasks + woodcutting_task_weight: WoodcuttingTaskWeight + minimum_general_tasks: MinimumGeneralTasks + general_task_weight: GeneralTaskWeight diff --git a/worlds/osrs/Regions.py b/worlds/osrs/Regions.py new file mode 100644 index 000000000000..436cdf3c7c78 --- /dev/null +++ b/worlds/osrs/Regions.py @@ -0,0 +1,12 @@ +import typing + + +class RegionRow(typing.NamedTuple): + name: str + itemReq: str + connections: typing.List[str] + resources: typing.List[str] + + +class ResourceRow(typing.NamedTuple): + name: str diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py new file mode 100644 index 000000000000..f726b4b81bf2 --- /dev/null +++ b/worlds/osrs/__init__.py @@ -0,0 +1,657 @@ +import typing + +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_rule, CollectionRule +from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ + chunksanity_special_region_names +from .Locations import OSRSLocation, LocationRow + +from .Options import OSRSOptions, StartingArea +from .Names import LocationNames, ItemNames, RegionNames + +from .LogicCSV.LogicCSVToPython import data_csv_tag +from .LogicCSV.items_generated import item_rows +from .LogicCSV.locations_generated import location_rows +from .LogicCSV.regions_generated import region_rows +from .LogicCSV.resources_generated import resource_rows +from .Regions import RegionRow, ResourceRow + + +class OSRSWeb(WebWorld): + theme = "stone" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld", + "English", + "docs/setup_en.md", + "setup/en", + ["digiholic"] + ) + tutorials = [setup_en] + + +class OSRSWorld(World): + game = "Old School Runescape" + options_dataclass = OSRSOptions + options: OSRSOptions + topology_present = True + web = OSRSWeb() + base_id = 0x070000 + data_version = 1 + + item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} + location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} + + region_name_to_data: typing.Dict[str, Region] + location_name_to_data: typing.Dict[str, OSRSLocation] + + location_rows_by_name: typing.Dict[str, LocationRow] + region_rows_by_name: typing.Dict[str, RegionRow] + resource_rows_by_name: typing.Dict[str, ResourceRow] + item_rows_by_name: typing.Dict[str, ItemRow] + + starting_area_item: str + + locations_by_category: typing.Dict[str, typing.List[LocationRow]] + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.region_name_to_data = {} + self.location_name_to_data = {} + + self.location_rows_by_name = {} + self.region_rows_by_name = {} + self.resource_rows_by_name = {} + self.item_rows_by_name = {} + + self.starting_area_item = "" + + self.locations_by_category = {} + + def generate_early(self) -> None: + location_categories = [location_row.category for location_row in location_rows] + self.locations_by_category = {category: + [location_row for location_row in location_rows if + location_row.category == category] + for category in location_categories} + + self.location_rows_by_name = {loc_row.name: loc_row for loc_row in location_rows} + self.region_rows_by_name = {reg_row.name: reg_row for reg_row in region_rows} + self.resource_rows_by_name = {rec_row.name: rec_row for rec_row in resource_rows} + self.item_rows_by_name = {it_row.name: it_row for it_row in item_rows} + + rnd = self.random + starting_area = self.options.starting_area + + if starting_area.value == StartingArea.option_any_bank: + self.starting_area_item = rnd.choice(starting_area_dict) + elif starting_area.value < StartingArea.option_chunksanity: + self.starting_area_item = starting_area_dict[starting_area.value] + else: + self.starting_area_item = rnd.choice(chunksanity_starting_chunks) + + # Set Starting Chunk + self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + + """ + This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. + _Make sure to update that value whenever the CSVs change!_ + """ + + def fill_slot_data(self): + data = self.options.as_dict("brutal_grinds") + data["data_csv_tag"] = data_csv_tag + return data + + def create_regions(self) -> None: + """ + called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done + during generate_early or basic as well. + """ + + # First, create the "Menu" region to start + menu_region = self.create_region("Menu") + + for region_row in region_rows: + self.create_region(region_row.name) + + for resource_row in resource_rows: + self.create_region(resource_row.name) + + # Removes the word "Area: " from the item name to get the region it applies to. + # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) + + # Create entrances between regions + for region_row in region_rows: + region = self.region_name_to_data[region_row.name] + + for outbound_region_name in region_row.connections: + parsed_outbound = outbound_region_name.replace('*', '') + entrance = region.create_exit(f"{region_row.name}->{parsed_outbound}") + entrance.connect(self.region_name_to_data[parsed_outbound]) + + item_name = self.region_rows_by_name[parsed_outbound].itemReq + if "*" not in outbound_region_name and "*" not in item_name: + entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) + continue + + self.generate_special_rules_for(entrance, region_row, outbound_region_name) + + for resource_region in region_row.resources: + if not resource_region: + continue + + entrance = region.create_exit(f"{region_row.name}->{resource_region.replace('*', '')}") + if "*" not in resource_region: + entrance.connect(self.region_name_to_data[resource_region]) + else: + self.generate_special_rules_for(entrance, region_row, resource_region) + entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + + self.roll_locations() + + def generate_special_rules_for(self, entrance, region_row, outbound_region_name): + # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") + if outbound_region_name == RegionNames.Cooks_Guild: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + cooking_level_rule = self.get_skill_rule("cooking", 32) + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + cooking_level_rule(state) + return + if outbound_region_name == RegionNames.Crafting_Guild: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + crafting_level_rule = self.get_skill_rule("crafting", 40) + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + crafting_level_rule(state) + return + if outbound_region_name == RegionNames.Corsair_Cove: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + # Need to be able to start Corsair Curse in addition to having the item + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + state.can_reach(RegionNames.Falador_Farm, "Region", self.player) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) + + return + if outbound_region_name == "Camdozaal*": + item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + state.has(ItemNames.QP_Below_Ice_Mountain, self.player) + return + if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) + return + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) + woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) + woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) + woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + # South of Varrock does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.Edgeville: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 57 + + if region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + # Lumbridge does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ + and self.options.max_woodcutting_level >= 12 + if outbound_region_name == RegionNames.Edgeville: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 42 + if region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ + and self.options.max_woodcutting_level >= 12 + # Edgeville does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + if region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 57 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + # Edgeville does not need to be checked, because it's already adjacent + + def roll_locations(self): + locations_required = 0 + generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + for item_row in item_rows: + locations_required += item_row.amount + + locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 + + # Quests are always added + for i, location_row in enumerate(location_rows): + if location_row.category in {"quest", "points", "goal"}: + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 + + # Build up the weighted Task Pool + rnd = self.random + + # Start with the minimum general tasks + general_tasks = [task for task in self.locations_by_category["general"]] + if not self.options.progressive_tasks: + rnd.shuffle(general_tasks) + else: + general_tasks.reverse() + for i in range(self.options.minimum_general_tasks): + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 + + tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {} + weights_per_task_type: typing.Dict[str, int] = {} + + task_types = ["prayer", "magic", "runecraft", "mining", "crafting", + "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] + for task_type in task_types: + max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") + max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") + tasks_for_this_type = [task for task in self.locations_by_category[task_type] + if task.skills[0].level <= max_level_for_task_type] + if not self.options.progressive_tasks: + rnd.shuffle(tasks_for_this_type) + else: + tasks_for_this_type.reverse() + + tasks_for_this_type = tasks_for_this_type[:max_amount_for_task_type] + weight_for_this_type = getattr(self.options, + f"{task_type}_task_weight") + if weight_for_this_type > 0 and tasks_for_this_type: + tasks_per_task_type[task_type] = tasks_for_this_type + weights_per_task_type[task_type] = weight_for_this_type + + # Build a list of collections and weights in a matching order for rnd.choices later + all_tasks = [] + all_weights = [] + for task_type in task_types: + if task_type in tasks_per_task_type: + all_tasks.append(tasks_per_task_type[task_type]) + all_weights.append(weights_per_task_type[task_type]) + + # Even after the initial forced generals, they can still be rolled randomly + if general_weight > 0: + all_tasks.append(general_tasks) + all_weights.append(general_weight) + + while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0): + if all_tasks: + chosen_task = rnd.choices(all_tasks, all_weights)[0] + if chosen_task: + task = chosen_task.pop() + self.add_location(task) + locations_added += 1 + + # This isn't an else because chosen_task can become empty in the process of resolving the above block + # We still want to clear this list out while we're doing that + if not chosen_task: + index = all_tasks.index(chosen_task) + del all_tasks[index] + del all_weights[index] + + else: + if len(general_tasks) == 0: + raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + + f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.") + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + def add_location(self, location): + index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] + self.create_and_add_location(index) + + def create_items(self) -> None: + for item_row in item_rows: + if item_row.name != self.starting_area_item: + for c in range(item_row.amount): + item = self.create_item(item_row.name) + self.multiworld.itempool.append(item) + + def get_filler_item_name(self) -> str: + return self.random.choice( + [ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, + ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon]) + + def create_and_add_location(self, row_index) -> None: + location_row = location_rows[row_index] + # print(f"Adding task {location_row.name}") + + # Create Location + location_id = self.base_id + row_index + if location_row.category == "points" or location_row.category == "goal": + location_id = None + location = OSRSLocation(self.player, location_row.name, location_id) + self.location_name_to_data[location_row.name] = location + + # Add the location to its first region, or if it doesn't belong to one, to Menu + region = self.region_name_to_data["Menu"] + if location_row.regions: + region = self.region_name_to_data[location_row.regions[0]] + location.parent_region = region + region.locations.append(location) + + def set_rules(self) -> None: + """ + called to set access and item rules on locations and entrances. + """ + quest_attr_names = ["Cooks_Assistant", "Demon_Slayer", "Restless_Ghost", "Romeo_Juliet", + "Sheep_Shearer", "Shield_of_Arrav", "Ernest_the_Chicken", "Vampyre_Slayer", + "Imp_Catcher", "Prince_Ali_Rescue", "Dorics_Quest", "Black_Knights_Fortress", + "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", + "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", + "Below_Ice_Mountain"] + for qp_attr_name in quest_attr_names: + loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") + item_name = getattr(ItemNames, f"QP_{qp_attr_name}") + self.multiworld.get_location(loc_name, self.player) \ + .place_locked_item(self.create_event(item_name)) + + for quest_attr_name in quest_attr_names: + qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") + add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( + self.multiworld.get_location(q_loc_name, self.player).can_reach(state) + )) + + # place "Victory" at "Dragon Slayer" and set collection as win condition + self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ + .place_locked_item(self.create_event("Victory")) + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Victory", self.player)) + + for location_name, location in self.location_name_to_data.items(): + location_row = self.location_rows_by_name[location_name] + # Set up requirements for region + for region_required_name in location_row.regions: + region_required = self.region_name_to_data[region_required_name] + add_rule(location, + lambda state, region_required=region_required: state.can_reach(region_required, "Region", + self.player)) + for skill_req in location_row.skills: + add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + for item_req in location_row.items: + add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) + if location_row.qp: + add_rule(location, lambda state, location_row=location_row: self.quest_points(state) > location_row.qp) + + def create_region(self, name: str) -> "Region": + region = Region(name, self.player, self.multiworld) + self.region_name_to_data[name] = region + self.multiworld.regions.append(region) + return region + + def create_item(self, item_name: str) -> "Item": + item = [item for item in item_rows if item.name == item_name][0] + index = item_rows.index(item) + return OSRSItem(item.name, item.progression, self.base_id + index, self.player) + + def create_event(self, event: str): + # while we are at it, we can also add a helper to create events + return OSRSItem(event, ItemClassification.progression, None, self.player) + + def quest_points(self, state): + qp = 0 + for qp_event in QP_Items: + if state.has(qp_event, self.player): + qp += int(qp_event[0]) + return qp + + """ + Ensures a target level can be reached with available resources + """ + + def get_skill_rule(self, skill, level) -> CollectionRule: + if skill.lower() == "fishing": + if self.options.brutal_grinds or level < 5: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) + if level < 20: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ + state.can_reach(RegionNames.Port_Sarim, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ + state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ + state.can_reach(RegionNames.Fly_Fish, "Region", self.player) + if skill.lower() == "mining": + if self.options.brutal_grinds or level < 15: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ + state.can_reach(RegionNames.Clay_Rock, "Region", self.player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or + state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) + if skill.lower() == "woodcutting": + if self.options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ + state.can_reach(RegionNames.Willow_Tree, "Region", self.player) + if skill.lower() == "smithing": + if self.options.brutal_grinds: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + (state.can_reach(RegionNames.Anvil, "Region", self.player) or + state.can_reach(RegionNames.Lumbridge, "Region", self.player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + state.can_reach(RegionNames.Anvil, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + state.can_reach(RegionNames.Anvil, "Region", self.player) + if skill.lower() == "crafting": + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ + state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) + + def can_pot(state): + return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) + + def can_tan(state): + return state.can_reach(RegionNames.Milk, "Region", self.player) and \ + state.can_reach(RegionNames.Al_Kharid, "Region", self.player) + + def mould_access(state): + return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ + state.can_reach(RegionNames.Rimmington, "Region", self.player) + + def can_silver(state): + + return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) + + def can_gold(state): + return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) + + if self.options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = self.get_skill_rule("smithing", 40) + can_smelt_silver = self.get_skill_rule("smithing", 20) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) + else: + return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ + (can_gold(state) and can_smelt_gold(state)) + if skill.lower() == "Cooking": + if self.options.brutal_grinds or level < 15: + return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ + state.can_reach(RegionNames.Egg, "Region", self.player) or \ + state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ + (state.can_reach(RegionNames.Wheat, "Region", self.player) and + state.can_reach(RegionNames.Windmill, "Region", self.player)) + else: + can_catch_fly_fish = self.get_skill_rule("fishing", 20) + return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ + can_catch_fly_fish(state) and \ + (state.can_reach(RegionNames.Milk, "Region", self.player) or + state.can_reach(RegionNames.Egg, "Region", self.player) or + state.can_reach(RegionNames.Shrimp, "Region", self.player) or + (state.can_reach(RegionNames.Wheat, "Region", self.player) and + state.can_reach(RegionNames.Windmill, "Region", self.player))) + if skill.lower() == "runecraft": + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) + if skill.lower() == "magic": + return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) + + return lambda state: True diff --git a/worlds/osrs/docs/en_Old School Runescape.md b/worlds/osrs/docs/en_Old School Runescape.md new file mode 100644 index 000000000000..d367082b2274 --- /dev/null +++ b/worlds/osrs/docs/en_Old School Runescape.md @@ -0,0 +1,114 @@ +# Old School Runescape + +## What is the Goal of this Randomizer? +The goal is to complete the quest "Dragon Slayer I" with limited access to gear and map chunks while following normal +Ironman/Group Ironman restrictions on a fresh free-to-play account. + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. OSRS contains many options for a highly customizable experience. The options available to you are: + +* **Starting Area** - The starting region of your run. This is the first region you will have available, and you can always +freely return to it (see the section below for when it is allowed to cross locked regions to access it) + * You may select a starting city from the list of Lumbridge, Al Kharid, Varrock (East or West), Edgeville, Falador, +Draynor Village, or The Wilderness (Ferox Enclave) + * The option "Any Bank" will choose one of the above regions at random + * The option "Chunksanity" can start you in _any_ chunk, regardless of whether it has access to a bank. +* **Brutal Grinds** - If enabled, the logic will assume you are willing to go to great lengths to train skills. + * As an example, when enabled, it might be in logic to obtain tin and copper from mob drops and smelt bronze bars to +reach Smithing Level 40 to smelt gold for a task. + * If left disabled, the logic will always ensure you have a reasonable method for training a skill to reach a specific +task, such as having access to intermediate-level training options +* **Progressive Tasks** - If enabled, tasks for a skill are generated in order from earliest to latest. + * For example, your first Smithing task would always be "Smelt an Iron Bar", then "Smelt a Silver Bar", and so on. +You would never have the task "Smelt a Gold Bar" without having every previous Smithing task as well. +This can lead to a more consistent length of run, and is generally shorter than disabling it, but with less variety. +* **Skill Category Weighting Options** + * These are available in each task category (all trainable skills plus "Combat" and "General") + * **Max [Category] Level** - The highest level you intend to have to reach in order to complete all tasks for this +category. For the Combat category, this is the max level of monster you are willing to fight. +General tasks do not have a level and thus do not have this option. + * **Max [Category] Tasks** - The highest number of tasks in this category you are willing to be assigned. +Note that you can end up with _less_ than this amount, but never more. The "General" category is used to fill remaining +spots so a maximum is not specified, instead it has a _minimum_ count. + * **[Category] Task Weighting** - The relative weighting of this category to all of the others. Increase this to make +tasks in this category more likely. + +## What does randomization do to this game? +The OSRS Archipelago Randomizer takes the form of a "Chunkman" account, a form of challenge account +where you are limited to specific regions of the map (known as "chunks") until you complete tasks to unlock +more. The plugin will interface with the [Region Locker Plugin](https://github.com/slaytostay/region-locker) to +visually display these chunk borders and highlight them as locked or unlocked. The optional included GPU plugin for the +Region Locker can tint the locked areas gray, but is incompatible with other GPU plugins such as 117's HD OSRS. +If you choose not to include it, the world map will show locked and unlocked regions instead. + +In order to access a region, you will need to access it entirely through unlocked regions. At no point are you +ever allowed to cross through locked regions, with the following exceptions: +* If your starting region is not Lumbridge, when you complete Tutorial Island, you will need to traverse locked regions +to reach your intended starting location. +* If your starting region is not Lumbridge, you are allowed to "Home Teleport" to your starting region by using the +Lumbridge Home Teleport Spell and then walking to your start location. This is to prevent you from getting "stuck" after +using one-way transportation such as the Port Sarim Jail Teleport from Shantay Pass and being locked out of progression. +* All of your starting Tutorial Island items are assumed to be available at all times. If you have lost an important +item such as a Tinderbox, and cannot re-obtain it in your unlocked region, you are allowed to enter locked regions to +replace it in the least obtrusive way possible. +* If you need to adjust Group Ironman settings, such as adding or removing a member, you may freely access The Node +to do so. + +When passing through locked regions for such exceptions, do not interact with any NPCs, items, or enemies and attempt +to spend as little time in them as possible. + +The plugin will prevent equipping items that you have not unlocked the ability to wield. For example, attempting +to equip an Iron Platebody before the first Progressive Armor unlock will display a chat message and will not +equip the item. + +The plugin will show a list of your current tasks in the sidebar. The plugin will be able to detect the completion +of most tasks, but in the case that a task cannot be detected (for example, killing an enemy with no +drop table such as Deadly Red Spiders), the task can be marked as complete manually by clicking +on the button. This button can also be used to mark completed tasks you have done while playing OSRS mobile or +on a different client without having the plugin available. Simply click the button the next time you are logged in to +Runelite and connected to send the check. + +Due to the nature of randomizing a live MMO with no ability to freely edit the character or adjust game logic or +balancing, this randomizer relies heavily on **the honor system**. The plugin cannot prevent you from walking through +locked regions or equipping locked items with the plugin disabled before connecting. It is important +to acknowledge before starting that the entire purpose of the randomizer is a self-imposed challenge, and there +is little point in cheating by circumventing the plugin's restrictions or marking a task complete without actually +completing it. If you wish to play OSRS with no restrictions, that is always available without the plugin. + +In order to access the AP Text Client commands (such as `!hint` or to chat with other players in the seed), enter your +command in chat prefaced by the string `!ap`. Example commands: + +`!ap buying gf 100k` -> Sends the message "buying gf 100k" to the server +`!ap !hint Area: Lumbridge` -> Attempts to hint for the "Area: Lumbridge" item. Results will appear in your chat box. + +Other server messages, such as chat, will appear in your chat box, prefaced by the Archipelago icon. + +## What items and locations get shuffled? +Items: +- Every map region (at least one chunk but sometimes more) +- Weapon tiers from iron to Rune (bronze is available from the start) +- Armor tiers from iron to Rune (bronze is available from the start) +- Two Spell Tiers (bolt and blast spells) +- Three tiers of Ranged Armor (leather, studded leather + vambraces, green dragonhide) +- Three tiers of Ranged Weapons (oak, willow, maple bows and their respective highest tier of arrows) + +Locations: +* Every Quest is a location that will always be included in every seed +* A random assortment of tasks, separated into categories based on the skill required. +These task categories can have different weights, minimums, and maximums based on your options. + * For a full list of Locations, items, and regions, see the +[Logic Document](https://docs.google.com/spreadsheets/d/1R8Cm8L6YkRWeiN7uYrdru8Vc1DlJ0aFAinH_fwhV8aU/edit?usp=sharing) + +## Which items can be in another player's world? +Any item or region unlock can be found in any player's world. + +## What does another world's item look like in Old School Runescape? +Upon completing a task, the item and recipient will be listed in the player's chatbox. + +## When the player receives an item, what happens? +In addition to the message appearing in the chatbox, a UI window will appear listing the item and who sent it. +These boxes also appear when connecting to a seed already in progress to list the items you have acquired while offline. +The sidebar will list all received items below the task list, starting with regions, then showing the highest tier of +equipment in each category. \ No newline at end of file diff --git a/worlds/osrs/docs/setup_en.md b/worlds/osrs/docs/setup_en.md new file mode 100644 index 000000000000..47c1c8f16fd7 --- /dev/null +++ b/worlds/osrs/docs/setup_en.md @@ -0,0 +1,58 @@ +# Setup Guide for Old School Runescape + +## Required Software + +- [RuneLite](https://runelite.net/) +- If the account being used has been migrated to a Jagex Account, the [Jagex Launcher](https://www.jagex.com/en-GB/launcher) +will also be necessary to run RuneLite + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can customize your settings by visiting the +[Old School Runescape Player Options Page](/games/Old%20School%20Runescape/player-options). + +## Joining a MultiWorld Game + +### Install the RuneLite Plugins +Open RuneLite and click on the wrench icon on the right side. From there, click on the plug icon to access the +Plugin Hub. You will need to install the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) +and [Region Locker Plugin](https://github.com/slaytostay/region-locker). The Region Locker plugin +will include three plugins; only the `Region Locker` plugin itself is required. The `Region Locker GPU` plugin can be +used to display locked chunks in gray, but is incompatible with other GPU plugins such as 117's HD OSRS and can be +disabled. + +### Create a new OSRS Account +The OSRS Randomizer assumes you are playing on a newly created f2p Ironman account. As such, you will need to [create a +new Runescape account](https://secure.runescape.com/m=account-creation/create_account?theme=oldschool). + +If you already have a [Jagex Account](https://www.jagex.com/en-GB/accounts) you can add up to 20 characters on +one account through the Jagex Launcher. Note that there is currently no way to _remove_ characters +from a Jagex Account, as such, you might want to create a separate account to hold your Archipelago +characters if you intend to use your main Jagex account for more characters in the future. + +**Protip**: In order to avoid having to remember random email addresses for many accounts, take advantage of an email +alias, a feature supported by most email providers. Any text after a `+` in your email address will redirect to your +normal address, but the email will be recognized by the Jagex login as a new email address. For example, if your email +were `Archipelago@gmail.com`, entering `Archipelago+OSRSRandomizer@gmail.com` would cause the confirmation email to +be sent to your primary address, but the alias can be used to create a new account. One recommendation would be to +include the date of generation in the account, such as `Archipelago+APYYMMDD@gmail.com` for easy memorability. + +After creating an account, you may run through Tutorial Island without connecting; the randomizer has no +effect on the Tutorial. + +### Connect to the Multiserver +In the Archipelago Plugin, enter your server information. The `Auto Reconnect on Login For` field should remain blank; +it will be populated by the character name you first connect with, and it will reconnect to the AP server whenever that +character logs in. Open the Archipelago panel on the right-hand side to connect to the multiworld while logged in to +a game world to associate this character to the randomizer. + +For further information about how to connect to the server in the RuneLite plugin, +please see the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) instructions. \ No newline at end of file From 6297a4efa552173f8a83f6dfa3b7f4fca828f4e4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 7 Aug 2024 12:01:41 -0400 Subject: [PATCH 120/393] TUNIC: Fix missing traversal req #3740 --- worlds/tunic/er_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index f49e7dff3e58..78a934b4904c 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1183,6 +1183,8 @@ class DeadEnd(IntEnum): [], "Library Hero's Grave Region": [], + "Library Hall to Rotunda": + [], }, "Library Hero's Grave Region": { "Library Hall": From cf6661439e006d17aaca3fb814da927e7a0bae09 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 7 Aug 2024 12:18:50 -0400 Subject: [PATCH 121/393] TUNIC: Sort entrances in the spoiler log (#3733) * Sort entrances in spoiler log * Rearrange portal list to closer match the vanilla game order, for better spoiler and because I already did this mod-side * Add break (thanks vi) --- worlds/tunic/er_data.py | 230 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 32 +++++- 2 files changed, 144 insertions(+), 118 deletions(-) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 78a934b4904c..e999026dec78 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -169,100 +169,16 @@ def destination_scene(self) -> str: # the vanilla connection destination="Overworld Redux", tag="_rafters"), Portal(name="Temple Door Exit", region="Sealed Temple", destination="Overworld Redux", tag="_main"), - - Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", - destination="Overworld Redux", tag="_entrance"), - Portal(name="Well to Well Boss", region="Beneath the Well Back", - destination="Sewer_Boss", tag="_"), - Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", - destination="Overworld Redux", tag="_west_aqueduct"), - - Portal(name="Well Boss to Well", region="Well Boss", - destination="Sewer", tag="_"), - Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", - destination="Crypt Redux", tag="_"), - - Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard", tag="_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux", tag="_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", destination="Overworld Redux", tag="_"), - Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", - destination="Furnace", tag="_"), - Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", - destination="Sewer_Boss", tag="_"), - - Portal(name="West Garden Exit near Hero's Grave", region="West Garden", - destination="Overworld Redux", tag="_lower"), - Portal(name="West Garden to Magic Dagger House", region="West Garden", - destination="archipelagos_house", tag="_"), - Portal(name="West Garden Exit after Boss", region="West Garden after Boss", - destination="Overworld Redux", tag="_upper"), - Portal(name="West Garden Shop", region="West Garden", - destination="Shop", tag="_"), - Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", - destination="Overworld Redux", tag="_lowest"), - Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="West Garden to Far Shore", region="West Garden Portal", - destination="Transit", tag="_teleporter_archipelagos_teleporter"), - - Portal(name="Magic Dagger House Exit", region="Magic Dagger House", - destination="Archipelagos Redux", tag="_"), - - Portal(name="Atoll Upper Exit", region="Ruined Atoll", - destination="Overworld Redux", tag="_upper"), - Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", - destination="Overworld Redux", tag="_lower"), - Portal(name="Atoll Shop", region="Ruined Atoll", - destination="Shop", tag="_"), - Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", - destination="Transit", tag="_teleporter_atoll"), - Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", - destination="Library Exterior", tag="_"), - Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", - destination="Frog Stairs", tag="_eye"), - Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", - destination="Frog Stairs", tag="_mouth"), - - Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", - destination="Atoll Redux", tag="_eye"), - Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", - destination="Atoll Redux", tag="_mouth"), - Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", - destination="frog cave main", tag="_Entrance"), - Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", - destination="frog cave main", tag="_Exit"), - - Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", - destination="Frog Stairs", tag="_Entrance"), - Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", - destination="Frog Stairs", tag="_Exit"), - - Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", - destination="Atoll Redux", tag="_"), - Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", - destination="Library Hall", tag="_"), - - Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", - destination="Library Exterior", tag="_"), - Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", - destination="Library Rotunda", tag="_"), - - Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", - destination="Library Hall", tag="_"), - Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", - destination="Library Lab", tag="_"), - - Portal(name="Library Lab to Rotunda", region="Library Lab Lower", - destination="Library Rotunda", tag="_"), - Portal(name="Library to Far Shore", region="Library Portal", - destination="Transit", tag="_teleporter_library teleporter"), - Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", - destination="Library Arena", tag="_"), - - Portal(name="Librarian Arena Exit", region="Library Arena", - destination="Library Lab", tag="_"), - + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room", tag="_"), + Portal(name="Forest to Belltower", region="East Forest", destination="Forest Belltower", tag="_"), Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", @@ -281,7 +197,14 @@ def destination_scene(self) -> str: # the vanilla connection destination="Sword Access", tag="_lower"), Portal(name="Forest Grave Path Upper Entrance", region="East Forest", destination="Sword Access", tag="_upper"), - + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux", tag="_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux", tag="_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", destination="East Forest Redux", tag="_upper"), Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", @@ -290,33 +213,54 @@ def destination_scene(self) -> str: # the vanilla connection destination="East Forest Redux", tag="_gate"), Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", destination="Forest Boss Room", tag="_"), - - Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", - destination="East Forest Redux", tag="_upper"), - Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", - destination="East Forest Redux", tag="_lower"), - Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", - destination="RelicVoid", tag="_teleporter_relic plinth"), - + Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", destination="East Forest Redux", tag="_lower"), Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", destination="East Forest Redux", tag="_upper"), - + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", destination="East Forest Redux Laddercave", tag="_"), Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", destination="Forest Belltower", tag="_"), + + Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", + destination="Overworld Redux", tag="_entrance"), + Portal(name="Well to Well Boss", region="Beneath the Well Back", + destination="Sewer_Boss", tag="_"), + Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", + destination="Overworld Redux", tag="_west_aqueduct"), - Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", - destination="Fortress Courtyard", tag="_"), - Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", - destination="East Forest Redux", tag="_"), - Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + Portal(name="Well Boss to Well", region="Well Boss", + destination="Sewer", tag="_"), + Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", + destination="Crypt Redux", tag="_"), + + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", destination="Overworld Redux", tag="_"), - Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", - destination="Forest Boss Room", tag="_"), + Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", + destination="Furnace", tag="_"), + Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", + destination="Sewer_Boss", tag="_"), + Portal(name="West Garden Exit near Hero's Grave", region="West Garden", + destination="Overworld Redux", tag="_lower"), + Portal(name="West Garden to Magic Dagger House", region="West Garden", + destination="archipelagos_house", tag="_"), + Portal(name="West Garden Exit after Boss", region="West Garden after Boss", + destination="Overworld Redux", tag="_upper"), + Portal(name="West Garden Shop", region="West Garden", + destination="Shop", tag="_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", + destination="Overworld Redux", tag="_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="West Garden to Far Shore", region="West Garden Portal", + destination="Transit", tag="_teleporter_archipelagos_teleporter"), + + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", + destination="Archipelagos Redux", tag="_"), + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", destination="Fortress Reliquary", tag="_Lower"), Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", @@ -333,12 +277,12 @@ def destination_scene(self) -> str: # the vanilla connection destination="Overworld Redux", tag="_"), Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", destination="Shop", tag="_"), - + Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", destination="Fortress Main", tag="_"), Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", destination="Fortress Courtyard", tag="_"), - + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", destination="Fortress Courtyard", tag="_Big Door"), Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", @@ -351,14 +295,14 @@ def destination_scene(self) -> str: # the vanilla connection destination="Fortress East", tag="_upper"), Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", destination="Fortress East", tag="_lower"), - + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", destination="Fortress Main", tag="_lower"), Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", destination="Fortress Courtyard", tag="_"), Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", destination="Fortress Main", tag="_upper"), - + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", destination="Fortress Courtyard", tag="_Lower"), Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", @@ -370,11 +314,67 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Dusty Exit", region="Fortress Leaf Piles", destination="Fortress Reliquary", tag="_"), - + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", destination="Fortress Main", tag="_"), Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", destination="Transit", tag="_teleporter_spidertank"), + + Portal(name="Atoll Upper Exit", region="Ruined Atoll", + destination="Overworld Redux", tag="_upper"), + Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", + destination="Overworld Redux", tag="_lower"), + Portal(name="Atoll Shop", region="Ruined Atoll", + destination="Shop", tag="_"), + Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", + destination="Transit", tag="_teleporter_atoll"), + Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", + destination="Library Exterior", tag="_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", + destination="Frog Stairs", tag="_eye"), + Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", + destination="Frog Stairs", tag="_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", + destination="Atoll Redux", tag="_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", + destination="Atoll Redux", tag="_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", + destination="frog cave main", tag="_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", + destination="frog cave main", tag="_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", + destination="Frog Stairs", tag="_Entrance"), + Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", + destination="Frog Stairs", tag="_Exit"), + + Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", + destination="Atoll Redux", tag="_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", + destination="Library Hall", tag="_"), + + Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", + destination="Library Exterior", tag="_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", + destination="Library Rotunda", tag="_"), + + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", + destination="Library Hall", tag="_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", + destination="Library Lab", tag="_"), + + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", + destination="Library Rotunda", tag="_"), + Portal(name="Library to Far Shore", region="Library Portal", + destination="Transit", tag="_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", + destination="Library Arena", tag="_"), + + Portal(name="Librarian Arena Exit", region="Library Arena", + destination="Library Lab", tag="_"), Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", destination="Mountaintop", tag="_"), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 0bd8c5e80681..a4295cf9f2a4 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -24,10 +24,10 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} if world.options.entrance_rando: portal_pairs = pair_portals(world) - # output the entrances to the spoiler log here for convenience - for portal1, portal2 in portal_pairs.items(): - world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + sorted_portal_pairs = sort_portals(portal_pairs) + for portal1, portal2 in sorted_portal_pairs.items(): + world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) else: portal_pairs = vanilla_portals() @@ -504,3 +504,29 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) return connected_regions + + +# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list +def sort_portals(portal_pairs: Dict[Portal, Portal]) -> Dict[str, str]: + sorted_pairs: Dict[str, str] = {} + reference_list: List[str] = [portal.name for portal in portal_mapping] + reference_list.append("Shop Portal") + + # note: this is not necessary yet since the shop portals aren't numbered yet -- they will be when decoupled happens + # due to plando, there can be a variable number of shops + # I could either do it like this, or just go up to like 200, this seemed better + # shop_count = 0 + # for portal1, portal2 in portal_pairs.items(): + # if portal1.name.startswith("Shop"): + # shop_count += 1 + # if portal2.name.startswith("Shop"): + # shop_count += 1 + # reference_list.extend([f"Shop Portal {i + 1}" for i in range(shop_count)]) + + for name in reference_list: + for portal1, portal2 in portal_pairs.items(): + if name == portal1.name: + sorted_pairs[portal1.name] = portal2.name + break + return sorted_pairs + From 74697b679ea4bd376647c69107891bb79b7b9c56 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:56:22 -0400 Subject: [PATCH 122/393] KH2: Update the docs to support steam in the setup guide (#3711) * doc updates * add steam link * Update worlds/kh2/docs/setup_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update setup_en.md * Forgot to include these * Consistent styling * :) * version 3.3.0 --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/kh2/docs/setup_en.md | 53 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index c6fdb020b8a4..ed4d90bb54fb 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -1,22 +1,25 @@ # Kingdom Hearts 2 Archipelago Setup Guide +

Quick Links

- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en) - [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)

Required Software:

- `Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) -- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
- 1. `3.2.0 OpenKH Mod Manager with Panacea`
- 2. `Lua Backend from the OpenKH Mod Manager` - 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
+`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/) + +- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) + 1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea` + 2. `Lua Backend from the OpenKH Mod Manager` + 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager` - Needed for Archipelago - 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
- 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
- 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
- 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
+ 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases) + 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager` + 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager` + 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager` 5. `AP Randomizer Seed` +

Required: Archipelago Companion Mod

Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`
@@ -24,6 +27,7 @@ Have this mod second-highest priority below the .zip seed.
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.

Required: Auto Save Mod

+ Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save

Installing A Seed

@@ -33,33 +37,33 @@ Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.

What the Mod Manager Should Look Like.

+ ![image](https://i.imgur.com/Si4oZ8w.png)

Using the KH2 Client

-Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`
Most checks will be sent to you anywhere outside a load or cutscene.
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.` -
+

KH2 Client should look like this:

+ ![image](https://i.imgur.com/qP6CmV8.png) -
-Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected +Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected

Common Pitfalls

+ - Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder. - - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) -
+ - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) - Not having Lua Backend Configured Correctly. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. -
-- Loading into Simulated Twilight Town Instead of the GOA. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. +- Loading into Simulated Twilight Town Instead of the GOA. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.

Best Practices

@@ -70,8 +74,11 @@ Enter `The room's port number` into the top box where the x's are and pr - Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed

Logic Sheet

+ Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing) +

F.A.Q.

+ - Why is my Client giving me a "Cannot Open Process: " error? - Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin. - Why is my HP/MP continuously increasing without stopping? @@ -83,11 +90,13 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Why did I not load into the correct visit? - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item. - What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`. + - Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`. - Why am I getting wallpapered while going into a world for the first time? - - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. + - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. - Why am I not getting magic? - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why did I crash after picking my dream weapon? + - This is normally caused by having an outdated GOA mod or having an outdated panacea and/or luabackend. To fix this rerun the setup wizard and reinstall luabackend and panacea. Also make sure all your mods are up-to-date. - Why did I crash? - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify @@ -99,5 +108,3 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. - How do I load an auto save? - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. - - From 05ce29f7dcad5af14cd2ffb89798695fc1c7c688 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Wed, 7 Aug 2024 22:57:07 +0100 Subject: [PATCH 123/393] RoR2: Remove recursion from explore mode access rules (#3681) The access rules for " Chest n", " Shrine n" etc. locations recursively called state.can_reach() for the n-1 location name, with the n=1 location being the only location to have the actual access rule set. This patch removes the recursion, instead setting the actual access rule directly on each location, increasing the performance of checking accessibility of n>1 locations. Risk of Rain 2 was already quite fast to generate despite the recursion in the access rules, but with this patch, generating a multiworld with 200 copies of the template RoR2 yaml (and progression balancing disabled through a meta.yaml) goes from about 18s to about 6s for me. From generating the same seed before and after this patch, the same result is produced. --- worlds/ror2/rules.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py index 2e6b018f42fb..f0ab9f28313f 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -31,23 +31,17 @@ def has_all_items(multiworld: MultiWorld, items: Set[str], region: str, player: # Checks to see if chest/shrine are accessible def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ -> None: - if item_number == 1: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) + location_name = f"{environment}: {item_type} {item_number}" + if item_type == "Scavenger": # scavengers need to be locked till after a full loop since that is when they are capable of spawning. # (While technically the requirement is just beating 5 stages, this will ensure that the player will have # a long enough run to have enough director credits for scavengers and # help prevent being stuck in the same stages until that point). - if item_type == "Scavenger": - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage 5", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: check_location(state, environment, player, item_number, item_type) - - -def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: - return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) def set_rules(ror2_world: "RiskOfRainWorld") -> None: From 575c338aa3c895aa4d10824be5380b4e094b55e1 Mon Sep 17 00:00:00 2001 From: Louis M Date: Wed, 7 Aug 2024 18:19:52 -0400 Subject: [PATCH 124/393] Aquaria: Logic bug fixes (#3679) * Fixing logic bugs * Require energy attack in the cathedral and energy form in the body * King Jelly can be beaten easily with only the Dual Form * I think that I have a problem with my left and right... * There is a monster that is blocking the path, soo need attack to pass * The Li cage is not accessible without the Sunken city boss * Removing useless space. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Two more minors logic modification * Adapting tests to af9b6cd * Reformat the Region file --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/aquaria/Items.py | 2 +- worlds/aquaria/Locations.py | 20 +- worlds/aquaria/Regions.py | 446 +++++++++--------- worlds/aquaria/test/__init__.py | 2 +- worlds/aquaria/test/test_beast_form_access.py | 24 +- ...test_beast_form_or_arnassi_armor_access.py | 39 ++ .../aquaria/test/test_energy_form_access.py | 53 +-- .../test_energy_form_or_dual_form_access.py | 92 ++++ worlds/aquaria/test/test_fish_form_access.py | 4 +- worlds/aquaria/test/test_light_access.py | 1 - .../aquaria/test/test_spirit_form_access.py | 1 - 11 files changed, 396 insertions(+), 288 deletions(-) create mode 100644 worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py create mode 100644 worlds/aquaria/test/test_energy_form_or_dual_form_access.py diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index 34557d95d00d..f822d675e6e7 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -99,7 +99,7 @@ def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup): "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha - "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume + "Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index 2eb9d1e9a29d..f6e098103fdc 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -45,7 +45,7 @@ class AquariaLocations: "Home Water, bulb below the grouper fish": 698058, "Home Water, bulb in the path below Nautilus Prime": 698059, "Home Water, bulb in the little room above the grouper fish": 698060, - "Home Water, bulb in the end of the left path from the Verse Cave": 698061, + "Home Water, bulb in the end of the path close to the Verse Cave": 698061, "Home Water, bulb in the top left path": 698062, "Home Water, bulb in the bottom left room": 698063, "Home Water, bulb close to Naija's Home": 698064, @@ -67,7 +67,7 @@ class AquariaLocations: locations_song_cave = { "Song Cave, Erulian spirit": 698206, - "Song Cave, bulb in the top left part": 698071, + "Song Cave, bulb in the top right part": 698071, "Song Cave, bulb in the big anemone room": 698072, "Song Cave, bulb in the path to the singing statues": 698073, "Song Cave, bulb under the rock in the path to the singing statues": 698074, @@ -152,6 +152,9 @@ class AquariaLocations: locations_arnassi_path = { "Arnassi Ruins, Arnassi Statue": 698164, + } + + locations_arnassi_cave_transturtle = { "Arnassi Ruins, Transturtle": 698217, } @@ -269,9 +272,12 @@ class AquariaLocations: } locations_forest_bl = { + "Kelp Forest bottom left area, Transturtle": 698212, + } + + locations_forest_bl_sc = { "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, Transturtle": 698212, } locations_forest_br = { @@ -370,7 +376,7 @@ class AquariaLocations: locations_sun_temple_r = { "Sun Temple, first bulb of the temple": 698091, - "Sun Temple, bulb on the left part": 698092, + "Sun Temple, bulb on the right part": 698092, "Sun Temple, bulb in the hidden room of the right part": 698093, "Sun Temple, Sun Key": 698182, } @@ -402,6 +408,9 @@ class AquariaLocations: "Abyss right area, bulb in the middle path": 698110, "Abyss right area, bulb behind the rock in the middle path": 698111, "Abyss right area, bulb in the left green room": 698112, + } + + locations_abyss_r_transturtle = { "Abyss right area, Transturtle": 698214, } @@ -499,6 +508,7 @@ class AquariaLocations: **AquariaLocations.locations_skeleton_path_sc, **AquariaLocations.locations_arnassi, **AquariaLocations.locations_arnassi_path, + **AquariaLocations.locations_arnassi_cave_transturtle, **AquariaLocations.locations_arnassi_crab_boss, **AquariaLocations.locations_sun_temple_l, **AquariaLocations.locations_sun_temple_r, @@ -509,6 +519,7 @@ class AquariaLocations: **AquariaLocations.locations_abyss_l, **AquariaLocations.locations_abyss_lb, **AquariaLocations.locations_abyss_r, + **AquariaLocations.locations_abyss_r_transturtle, **AquariaLocations.locations_energy_temple_1, **AquariaLocations.locations_energy_temple_2, **AquariaLocations.locations_energy_temple_3, @@ -530,6 +541,7 @@ class AquariaLocations: **AquariaLocations.locations_forest_tr, **AquariaLocations.locations_forest_tr_fp, **AquariaLocations.locations_forest_bl, + **AquariaLocations.locations_forest_bl_sc, **AquariaLocations.locations_forest_br, **AquariaLocations.locations_forest_boss, **AquariaLocations.locations_forest_boss_entrance, diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 93c02d4e6766..3ec1fb880e13 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -14,97 +14,112 @@ # Every condition to connect regions -def _has_hot_soup(state:CollectionState, player: int) -> bool: +def _has_hot_soup(state: CollectionState, player: int) -> bool: """`player` in `state` has the hotsoup item""" - return state.has("Hot soup", player) + return state.has_any({"Hot soup", "Hot soup x 2"}, player) -def _has_tongue_cleared(state:CollectionState, player: int) -> bool: +def _has_tongue_cleared(state: CollectionState, player: int) -> bool: """`player` in `state` has the Body tongue cleared item""" return state.has("Body tongue cleared", player) -def _has_sun_crystal(state:CollectionState, player: int) -> bool: +def _has_sun_crystal(state: CollectionState, player: int) -> bool: """`player` in `state` has the Sun crystal item""" return state.has("Has sun crystal", player) and _has_bind_song(state, player) -def _has_li(state:CollectionState, player: int) -> bool: +def _has_li(state: CollectionState, player: int) -> bool: """`player` in `state` has Li in its team""" return state.has("Li and Li song", player) -def _has_damaging_item(state:CollectionState, player: int) -> bool: +def _has_damaging_item(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", - "Baby Piranha", "Baby Blaster"}, player) + return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", + "Baby Piranha", "Baby Blaster"}, player) -def _has_shield_song(state:CollectionState, player: int) -> bool: +def _has_energy_attack_item(state: CollectionState, player: int) -> bool: + """`player` in `state` has items that can do a lot of damage (enough to beat bosses)""" + return _has_energy_form(state, player) or _has_dual_form(state, player) + + +def _has_shield_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" return state.has("Shield song", player) -def _has_bind_song(state:CollectionState, player: int) -> bool: +def _has_bind_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the bind song item""" return state.has("Bind song", player) -def _has_energy_form(state:CollectionState, player: int) -> bool: +def _has_energy_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the energy form item""" return state.has("Energy form", player) -def _has_beast_form(state:CollectionState, player: int) -> bool: +def _has_beast_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the beast form item""" return state.has("Beast form", player) -def _has_nature_form(state:CollectionState, player: int) -> bool: +def _has_beast_and_soup_form(state: CollectionState, player: int) -> bool: + """`player` in `state` has the beast form item""" + return _has_beast_form(state, player) and _has_hot_soup(state, player) + + +def _has_beast_form_or_arnassi_armor(state: CollectionState, player: int) -> bool: + """`player` in `state` has the beast form item""" + return _has_beast_form(state, player) or state.has("Arnassi Armor", player) + + +def _has_nature_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the nature form item""" return state.has("Nature form", player) -def _has_sun_form(state:CollectionState, player: int) -> bool: +def _has_sun_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the sun form item""" return state.has("Sun form", player) -def _has_light(state:CollectionState, player: int) -> bool: +def _has_light(state: CollectionState, player: int) -> bool: """`player` in `state` has the light item""" return state.has("Baby Dumbo", player) or _has_sun_form(state, player) -def _has_dual_form(state:CollectionState, player: int) -> bool: +def _has_dual_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the dual form item""" return _has_li(state, player) and state.has("Dual form", player) -def _has_fish_form(state:CollectionState, player: int) -> bool: +def _has_fish_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the fish form item""" return state.has("Fish form", player) -def _has_spirit_form(state:CollectionState, player: int) -> bool: +def _has_spirit_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the spirit form item""" return state.has("Spirit form", player) -def _has_big_bosses(state:CollectionState, player: int) -> bool: +def _has_big_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated", - "Sun God beated", "The Golem beated"}, player) + "Sun God beated", "The Golem beated"}, player) -def _has_mini_bosses(state:CollectionState, player: int) -> bool: +def _has_mini_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated", - "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated", - "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player) + "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated", + "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player) -def _has_secrets(state:CollectionState, player: int) -> bool: - return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"},player) +def _has_secrets(state: CollectionState, player: int) -> bool: + return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"}, player) class AquariaRegions: @@ -134,6 +149,7 @@ class AquariaRegions: skeleton_path: Region skeleton_path_sc: Region arnassi: Region + arnassi_cave_transturtle: Region arnassi_path: Region arnassi_crab_boss: Region simon: Region @@ -152,6 +168,7 @@ class AquariaRegions: forest_tr: Region forest_tr_fp: Region forest_bl: Region + forest_bl_sc: Region forest_br: Region forest_boss: Region forest_boss_entrance: Region @@ -179,6 +196,7 @@ class AquariaRegions: abyss_l: Region abyss_lb: Region abyss_r: Region + abyss_r_transturtle: Region ice_cave: Region bubble_cave: Region bubble_cave_boss: Region @@ -213,7 +231,7 @@ class AquariaRegions: """ def __add_region(self, hint: str, - locations: Optional[Dict[str, Optional[int]]]) -> Region: + locations: Optional[Dict[str, int]]) -> Region: """ Create a new Region, add it to the `world` regions and return it. Be aware that this function have a side effect on ``world`.`regions` @@ -236,7 +254,7 @@ def __create_home_water_area(self) -> None: self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest", AquariaLocations.locations_home_water_nautilus) self.home_water_transturtle = self.__add_region("Home Water, turtle room", - AquariaLocations.locations_home_water_transturtle) + AquariaLocations.locations_home_water_transturtle) self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) @@ -280,6 +298,8 @@ def __create_openwater(self) -> None: self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", AquariaLocations.locations_arnassi_path) + self.arnassi_cave_transturtle = self.__add_region("Arnassi Ruins, transturtle area", + AquariaLocations.locations_arnassi_cave_transturtle) self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair", AquariaLocations.locations_arnassi_crab_boss) @@ -302,9 +322,9 @@ def __create_mithalas(self) -> None: AquariaLocations.locations_cathedral_r) 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", + self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", None) + self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", AquariaLocations.locations_cathedral_boss) - self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", None) def __create_forest(self) -> None: """ @@ -320,6 +340,8 @@ def __create_forest(self) -> None: AquariaLocations.locations_forest_tr_fp) self.forest_bl = self.__add_region("Kelp Forest bottom left area", AquariaLocations.locations_forest_bl) + self.forest_bl_sc = self.__add_region("Kelp Forest bottom left area, spirit crystals", + AquariaLocations.locations_forest_bl_sc) self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", @@ -375,9 +397,9 @@ def __create_sun_temple(self) -> None: self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", - AquariaLocations.locations_sun_temple_boss_path) + AquariaLocations.locations_sun_temple_boss_path) self.sun_temple_boss = self.__add_region("Sun Temple boss area", - AquariaLocations.locations_sun_temple_boss) + AquariaLocations.locations_sun_temple_boss) def __create_abyss(self) -> None: """ @@ -388,6 +410,8 @@ def __create_abyss(self) -> None: AquariaLocations.locations_abyss_l) self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb) self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) + self.abyss_r_transturtle = self.__add_region("Abyss right area, transturtle", + AquariaLocations.locations_abyss_r_transturtle) self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) @@ -407,7 +431,7 @@ def __create_sunken_city(self) -> None: self.sunken_city_r = self.__add_region("Sunken City right area", AquariaLocations.locations_sunken_city_r) self.sunken_city_boss = self.__add_region("Sunken City boss area", - AquariaLocations.locations_sunken_city_boss) + AquariaLocations.locations_sunken_city_boss) def __create_body(self) -> None: """ @@ -427,7 +451,7 @@ def __create_body(self) -> None: self.final_boss_tube = self.__add_region("The Body, final boss area turtle room", AquariaLocations.locations_final_boss_tube) self.final_boss = self.__add_region("The Body, final boss", - AquariaLocations.locations_final_boss) + AquariaLocations.locations_final_boss) self.final_boss_end = self.__add_region("The Body, final boss area", None) def __connect_one_way_regions(self, source_name: str, destination_name: str, @@ -455,8 +479,8 @@ def __connect_home_water_regions(self) -> None: """ Connect entrances of the different regions around `home_water` """ - self.__connect_regions("Menu", "Verse Cave right area", - self.menu, self.verse_cave_r) + self.__connect_one_way_regions("Menu", "Verse Cave right area", + self.menu, self.verse_cave_r) self.__connect_regions("Verse Cave left area", "Verse Cave right area", self.verse_cave_l, self.verse_cave_r) self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) @@ -464,7 +488,8 @@ def __connect_home_water_regions(self) -> None: self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) self.__connect_regions("Home Water", "Home Water, nautilus nest", self.home_water, self.home_water_nautilus, - lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player) and + _has_bind_song(state, self.player)) self.__connect_regions("Home Water", "Home Water transturtle room", self.home_water, self.home_water_transturtle) self.__connect_regions("Home Water", "Energy Temple first area", @@ -472,7 +497,7 @@ def __connect_home_water_regions(self) -> None: lambda state: _has_bind_song(state, self.player)) self.__connect_regions("Home Water", "Energy Temple_altar", self.home_water, self.energy_temple_altar, - lambda state: _has_energy_form(state, self.player) and + lambda state: _has_energy_attack_item(state, self.player) and _has_bind_song(state, self.player)) self.__connect_regions("Energy Temple first area", "Energy Temple second area", self.energy_temple_1, self.energy_temple_2, @@ -482,28 +507,28 @@ def __connect_home_water_regions(self) -> None: lambda state: _has_fish_form(state, self.player)) self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", self.energy_temple_idol, self.energy_temple_boss, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player) and + _has_fish_form(state, self.player)) self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", self.energy_temple_1, self.energy_temple_boss, lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", self.energy_temple_boss, self.energy_temple_1, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_regions("Energy Temple second area", "Energy Temple third area", self.energy_temple_2, self.energy_temple_3, - lambda state: _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", self.energy_temple_boss, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", self.energy_temple_1, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_beast_form(state, self.player)) self.__connect_regions("Home Water", "Open Water top left area", self.home_water, self.openwater_tl) @@ -520,7 +545,7 @@ def __connect_open_water_regions(self) -> None: self.openwater_tl, self.forest_br) self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", self.openwater_tr, self.openwater_tr_turtle, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_regions("Open Water top right area", "Open Water bottom right area", self.openwater_tr, self.openwater_br) self.__connect_regions("Open Water top right area", "Mithalas City", @@ -529,10 +554,9 @@ def __connect_open_water_regions(self) -> None: self.openwater_tr, self.veil_bl) self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", self.openwater_tr, self.veil_br, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", - self.veil_br, self.openwater_tr, - lambda state: _has_beast_form(state, self.player)) + self.veil_br, self.openwater_tr) self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", self.openwater_bl, self.openwater_br) self.__connect_regions("Open Water bottom left area", "Skeleton path", @@ -551,10 +575,14 @@ def __connect_open_water_regions(self) -> None: self.arnassi, self.openwater_br) self.__connect_regions("Arnassi", "Arnassi path", self.arnassi, self.arnassi_path) + self.__connect_regions("Arnassi ruins, transturtle area", "Arnassi path", + self.arnassi_cave_transturtle, self.arnassi_path, + lambda state: _has_fish_form(state, self.player)) self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area", self.arnassi_path, self.arnassi_crab_boss, - lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player) and + (_has_energy_attack_item(state, self.player) or + _has_nature_form(state, self.player))) self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path", self.arnassi_crab_boss, self.arnassi_path) @@ -564,61 +592,62 @@ def __connect_mithalas_regions(self) -> None: """ self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", self.mithalas_city, self.mithalas_city_top_path, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", self.mithalas_city_top_path, self.mithalas_city) self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) self.__connect_regions("Mithalas City", "Mithalas castle", - self.mithalas_city, self.cathedral_l, - lambda state: _has_fish_form(state, self.player)) + self.mithalas_city, self.cathedral_l) self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", self.mithalas_city_top_path, self.cathedral_l_tube, lambda state: _has_nature_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", self.cathedral_l_tube, self.mithalas_city_top_path, - lambda state: _has_beast_form(state, self.player) and - _has_nature_form(state, self.player)) + lambda state: _has_nature_form(state, self.player)) self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals", - self.cathedral_l_tube, self.cathedral_l_sc, - lambda state: _has_spirit_form(state, self.player)) + self.cathedral_l_tube, self.cathedral_l_sc, + lambda state: _has_spirit_form(state, self.player)) self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle", - self.cathedral_l_tube, self.cathedral_l, - lambda state: _has_spirit_form(state, self.player)) + self.cathedral_l_tube, self.cathedral_l, + lambda state: _has_spirit_form(state, self.player)) self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals", self.cathedral_l, self.cathedral_l_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral boss left area", - self.cathedral_l, self.cathedral_boss_l, - lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player) and - _has_bind_song(state, self.player)) + self.__connect_one_way_regions("Mithalas castle", "Cathedral boss right area", + self.cathedral_l, self.cathedral_boss_r, + lambda state: _has_beast_form(state, self.player)) + self.__connect_one_way_regions("Cathedral boss left area", "Mithalas castle", + self.cathedral_boss_l, self.cathedral_l, + lambda state: _has_beast_form(state, self.player)) 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", "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("Mithalas Cathedral", "Mithalas Cathedral underground", - self.cathedral_r, self.cathedral_underground, - lambda state: _has_energy_form(state, self.player)) - 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", "Mithalas Cathedral underground", + lambda state: _has_beast_form(state, self.player)) + self.__connect_one_way_regions("Mithalas castle", "Mithalas Cathedral", + self.cathedral_l, self.cathedral_r, + lambda state: _has_bind_song(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Mithalas Cathedral", "Mithalas Cathedral underground", + self.cathedral_r, self.cathedral_underground) + self.__connect_one_way_regions("Mithalas Cathedral underground", "Mithalas Cathedral", + self.cathedral_underground, self.cathedral_r, + lambda state: _has_beast_form(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss right area", + self.cathedral_underground, self.cathedral_boss_r) + self.__connect_one_way_regions("Cathedral boss right 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", + self.__connect_one_way_regions("Cathedral boss right area", "Cathedral boss left area", self.cathedral_boss_r, self.cathedral_boss_l, lambda state: _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Cathedral boss left area", "Cathedral boss right area", + self.cathedral_boss_l, self.cathedral_boss_r) def __connect_forest_regions(self) -> None: """ @@ -628,6 +657,12 @@ def __connect_forest_regions(self) -> None: self.forest_br, self.veil_bl) self.__connect_regions("Forest bottom right", "Forest bottom left area", self.forest_br, self.forest_bl) + self.__connect_one_way_regions("Forest bottom left area", "Forest bottom left area, spirit crystals", + self.forest_bl, self.forest_bl_sc, + lambda state: _has_energy_attack_item(state, self.player) or + _has_fish_form(state, self.player)) + self.__connect_one_way_regions("Forest bottom left area, spirit crystals", "Forest bottom left area", + self.forest_bl_sc, self.forest_bl) self.__connect_regions("Forest bottom right", "Forest top right area", self.forest_br, self.forest_tr) self.__connect_regions("Forest bottom left area", "Forest fish cave", @@ -641,7 +676,7 @@ def __connect_forest_regions(self) -> None: self.forest_tl, self.forest_tl_fp, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_fish_form(state, self.player)) self.__connect_regions("Forest top left area", "Forest top right area", self.forest_tl, self.forest_tr) @@ -649,7 +684,7 @@ def __connect_forest_regions(self) -> None: self.forest_tl, self.forest_boss_entrance) self.__connect_regions("Forest boss area", "Forest boss entrance", self.forest_boss, self.forest_boss_entrance, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_regions("Forest top right area", "Forest top right area fish pass", self.forest_tr, self.forest_tr_fp, lambda state: _has_fish_form(state, self.player)) @@ -663,7 +698,7 @@ def __connect_forest_regions(self) -> None: self.__connect_regions("Fermog cave", "Fermog boss", self.mermog_cave, self.mermog_boss, lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) def __connect_veil_regions(self) -> None: """ @@ -681,8 +716,7 @@ def __connect_veil_regions(self) -> None: self.veil_b_sc, self.veil_br, lambda state: _has_spirit_form(state, self.player)) self.__connect_regions("Veil bottom right", "Veil top left area", - self.veil_br, self.veil_tl, - lambda state: _has_beast_form(state, self.player)) + self.veil_br, self.veil_tl) self.__connect_regions("Veil top left area", "Veil_top left area, fish pass", self.veil_tl, self.veil_tl_fp, lambda state: _has_fish_form(state, self.player)) @@ -691,20 +725,25 @@ def __connect_veil_regions(self) -> None: self.__connect_regions("Veil top left area", "Turtle cave", self.veil_tl, self.turtle_cave) self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", - self.turtle_cave, self.turtle_cave_bubble, - lambda state: _has_beast_form(state, self.player)) + self.turtle_cave, self.turtle_cave_bubble) self.__connect_regions("Veil right of sun temple", "Sun Temple right area", self.veil_tr_r, self.sun_temple_r) - self.__connect_regions("Sun Temple right area", "Sun Temple left area", - self.sun_temple_r, self.sun_temple_l, - lambda state: _has_bind_song(state, self.player)) + self.__connect_one_way_regions("Sun Temple right area", "Sun Temple left area", + self.sun_temple_r, self.sun_temple_l, + lambda state: _has_bind_song(state, self.player) or + _has_light(state, self.player)) + self.__connect_one_way_regions("Sun Temple left area", "Sun Temple right area", + self.sun_temple_l, self.sun_temple_r, + lambda state: _has_light(state, self.player)) self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", - self.sun_temple_l, self.sun_temple_boss_path) + self.sun_temple_l, self.sun_temple_boss_path, + lambda state: _has_light(state, self.player) or + _has_sun_crystal(state, self.player)) self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", self.sun_temple_boss, self.veil_tr_l) self.__connect_regions("Veil left of sun temple", "Octo cave top path", @@ -712,7 +751,7 @@ def __connect_veil_regions(self) -> None: lambda state: _has_fish_form(state, self.player) and _has_sun_form(state, self.player) and _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Veil left of sun temple", "Octo cave bottom path", self.veil_tr_l, self.octo_cave_b, lambda state: _has_fish_form(state, self.player)) @@ -728,16 +767,22 @@ def __connect_abyss_regions(self) -> None: self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) self.__connect_one_way_regions("Abyss left bottom area", "Body center area", - self.abyss_lb, self.body_c, - lambda state: _has_tongue_cleared(state, self.player)) + self.abyss_lb, self.body_c, + lambda state: _has_tongue_cleared(state, self.player)) self.__connect_one_way_regions("Body center area", "Abyss left bottom area", - self.body_c, self.abyss_lb) + self.body_c, self.abyss_lb) self.__connect_regions("Abyss left area", "King jellyfish cave", self.abyss_l, self.king_jellyfish_cave, - lambda state: _has_energy_form(state, self.player) and - _has_beast_form(state, self.player)) + lambda state: (_has_energy_form(state, self.player) and + _has_beast_form(state, self.player)) or + _has_dual_form(state, self.player)) self.__connect_regions("Abyss left area", "Abyss right area", self.abyss_l, self.abyss_r) + self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle", + self.abyss_r, self.abyss_r_transturtle) + self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area", + self.abyss_r_transturtle, self.abyss_r, + lambda state: _has_light(state, self.player)) self.__connect_regions("Abyss right area", "Inside the whale", self.abyss_r, self.whale, lambda state: _has_spirit_form(state, self.player) and @@ -747,13 +792,14 @@ def __connect_abyss_regions(self) -> None: lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Abyss right area", "Ice Cave", self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Bubble Cave", + self.__connect_regions("Ice cave", "Bubble Cave", self.ice_cave, self.bubble_cave, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form(state, self.player) or + _has_hot_soup(state, self.player)) self.__connect_regions("Bubble Cave boss area", "Bubble Cave", self.bubble_cave, self.bubble_cave_boss, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) @@ -772,7 +818,7 @@ def __connect_sunken_city_regions(self) -> None: self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_sun_form(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_bind_song(state, self.player)) def __connect_body_regions(self) -> None: @@ -780,11 +826,13 @@ def __connect_body_regions(self) -> None: Connect entrances of the different regions around The Body """ self.__connect_regions("Body center area", "Body left area", - self.body_c, self.body_l) + self.body_c, self.body_l, + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Body center area", "Body right area top path", self.body_c, self.body_rt) self.__connect_regions("Body center area", "Body right area bottom path", - self.body_c, self.body_rb) + self.body_c, self.body_rb, + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Body center area", "Body bottom area", self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) @@ -803,22 +851,12 @@ def __connect_body_regions(self) -> None: self.__connect_one_way_regions("final boss third form area", "final boss end", self.final_boss, self.final_boss_end) - def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region, - rule=None) -> None: + def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, + region_target: Region) -> None: """Connect a single transturtle to another one""" if item_source != item_target: - if rule is None: - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, - lambda state: state.has(item_target, self.player)) - else: - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, rule) - - def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region, - region_target: Region) -> None: - """Connect the Arnassi Ruins transturtle to another one""" - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, - lambda state: state.has(item_target, self.player) and - _has_fish_form(state, self.player)) + self.__connect_one_way_regions(item_source, item_target, region_source, region_target, + lambda state: state.has(item_target, self.player)) def _connect_transturtle_to_other(self, item: str, region: Region) -> None: """Connect a single transturtle to all others""" @@ -827,24 +865,10 @@ def _connect_transturtle_to_other(self, item: str, region: Region) -> None: self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) - self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) + self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r_transturtle) self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path, - lambda state: state.has("Transturtle Arnassi Ruins", self.player) and - _has_fish_form(state, self.player)) - - def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None: - """Connect the Arnassi Ruins transturtle to all others""" - self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) - self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region, - self.openwater_tr_turtle) - self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) - self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) - self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon) + self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_cave_transturtle) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" @@ -853,10 +877,10 @@ def __connect_transturtles(self) -> None: self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) - self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r) + self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r_transturtle) self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) - self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path) + self._connect_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_cave_transturtle) def connect_regions(self) -> None: """ @@ -893,7 +917,7 @@ def __add_event_big_bosses(self) -> None: self.__add_event_location(self.energy_temple_boss, "Beating Fallen God", "Fallen God beated") - self.__add_event_location(self.cathedral_boss_r, + self.__add_event_location(self.cathedral_boss_l, "Beating Mithalan God", "Mithalan God beated") self.__add_event_location(self.forest_boss, @@ -970,8 +994,9 @@ def __adjusting_urns_rules(self) -> None: """Since Urns need to be broken, add a damaging item to rules""" add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), - lambda state: _has_damaging_item(state, self.player)) + add_rule( + self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), + lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), @@ -1019,66 +1044,46 @@ def __adjusting_soup_rules(self) -> None: Modify rules for location that need soup """ add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) + lambda state: _has_hot_soup(state, self.player)) add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) + lambda state: _has_beast_and_soup_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Open Water top right to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Veil top right to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), @@ -1097,12 +1102,14 @@ def __adjusting_light_in_dark_place_rules(self) -> None: def __adjusting_manual_rules(self) -> None: add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), 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("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), 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)) + 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)) add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), @@ -1114,103 +1121,119 @@ def __adjusting_manual_rules(self) -> None: add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", - self.player), lambda state: _has_energy_form(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), - lambda state: _has_fish_form(state, self.player) and - _has_spirit_form(state, self.player)) + lambda state: _has_fish_form(state, self.player) or + _has_beast_and_soup_form(state, self.player)) + add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), + lambda state: _has_damaging_item(state, self.player)) + add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), + lambda state: _has_damaging_item(state, self.player)) + add_rule(self.multiworld.get_location( + "The Veil top right area, bulb in the middle of the wall jump cliff", self.player + ), lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) + add_rule(self.multiworld.get_location("Kelp Forest top left area, Jelly Egg", self.player), + lambda state: _has_beast_form(state, self.player)) + add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), + lambda state: state.has("Sun God beated", self.player)) + add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), + lambda state: state.has("Sun God beated", self.player)) + add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), + lambda state: _has_tongue_cleared(state, self.player)) def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mithalas boss area, beating Mithalan God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple boss area, beating Sun God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sunken City, bulb on top of the boss area", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Home Water, Nautilus Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mithalas City Castle, beating the Priests", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mermog cave, Piranha Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Octopus Cave, Dumbo Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Final Boss area, bulb in the boss third form room", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, first cliff bulb", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, second cliff bulb", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, Verse Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, Sun Key", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("The Body bottom area, Mutant Costume", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression def adjusting_rules(self, options: AquariaOptions) -> None: """ Modify rules for single location or optional rules """ + self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player) self.__adjusting_urns_rules() self.__adjusting_crates_rules() self.__adjusting_soup_rules() @@ -1234,7 +1257,7 @@ def adjusting_rules(self, options: AquariaOptions) -> None: lambda state: _has_bind_song(state, self.player)) if options.unconfine_home_water.value in [0, 2]: add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), - lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) + lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) if options.early_energy_form: self.multiworld.early_items[self.player]["Energy form"] = 1 @@ -1274,6 +1297,7 @@ def __add_open_water_regions_to_world(self) -> None: self.multiworld.regions.append(self.arnassi) self.multiworld.regions.append(self.arnassi_path) self.multiworld.regions.append(self.arnassi_crab_boss) + self.multiworld.regions.append(self.arnassi_cave_transturtle) self.multiworld.regions.append(self.simon) def __add_mithalas_regions_to_world(self) -> None: @@ -1300,6 +1324,7 @@ def __add_forest_regions_to_world(self) -> None: self.multiworld.regions.append(self.forest_tr) self.multiworld.regions.append(self.forest_tr_fp) self.multiworld.regions.append(self.forest_bl) + self.multiworld.regions.append(self.forest_bl_sc) self.multiworld.regions.append(self.forest_br) self.multiworld.regions.append(self.forest_boss) self.multiworld.regions.append(self.forest_boss_entrance) @@ -1337,6 +1362,7 @@ def __add_abyss_regions_to_world(self) -> None: self.multiworld.regions.append(self.abyss_l) self.multiworld.regions.append(self.abyss_lb) self.multiworld.regions.append(self.abyss_r) + self.multiworld.regions.append(self.abyss_r_transturtle) self.multiworld.regions.append(self.ice_cave) self.multiworld.regions.append(self.bubble_cave) self.multiworld.regions.append(self.bubble_cave_boss) diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 029db691b66b..8c4f64c3452c 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -141,7 +141,7 @@ "Sun Temple, bulb at the top of the high dark room", "Sun Temple, Golden Gear", "Sun Temple, first bulb of the temple", - "Sun Temple, bulb on the left part", + "Sun Temple, bulb on the right part", "Sun Temple, bulb in the hidden room of the right part", "Sun Temple, Sun Key", "Sun Worm path, first path bulb", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index 0efc3e7388fe..c09586269d38 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -13,36 +13,16 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mithalas City Castle, beating the Priests", - "Arnassi Ruins, Crab Armor", - "Arnassi Ruins, Song Plant Spore", - "Mithalas City, first bulb at the end of the top path", - "Mithalas City, second bulb at the end of the top path", - "Mithalas City, bulb in the top path", - "Mithalas City, Mithalas Pot", - "Mithalas City, urn in the Castle flower tube entrance", "Mermog cave, Piranha Egg", + "Kelp Forest top left area, Jelly Egg", "Mithalas Cathedral, Mithalan Dress", - "Turtle cave, bulb in Bubble Cliff", - "Turtle cave, Urchin Costume", - "Sun Worm path, first cliff bulb", - "Sun Worm path, second cliff bulb", "The Veil top right area, bulb at the top of the waterfall", - "Bubble Cave, bulb in the left cave wall", - "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - "Bubble Cave, Verse Egg", "Sunken City, bulb on top of the boss area", "Octopus Cave, Dumbo Egg", "Beating the Golem", "Beating Mergog", - "Beating Crabbius Maximus", "Beating Octopus Prime", - "Beating Mantis Shrimp Prime", - "King Jellyfish Cave, Jellyfish Costume", - "King Jellyfish Cave, bulb in the right path from King Jelly", - "Beating King Jellyfish God Prime", - "Beating Mithalan priests", - "Sunken City cleared" + "Sunken City cleared", ] items = [["Beast form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py new file mode 100644 index 000000000000..fa4c6923400a --- /dev/null +++ b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py @@ -0,0 +1,39 @@ +""" +Author: Louis M +Date: Thu, 18 Apr 2024 18:45:56 +0000 +Description: Unit test used to test accessibility of locations with and without the beast form or arnassi armor +""" + +from . import AquariaTestBase + + +class BeastForArnassiArmormAccessTest(AquariaTestBase): + """Unit test used to test accessibility of locations with and without the beast form or arnassi armor""" + + def test_beast_form_arnassi_armor_location(self) -> None: + """Test locations that require beast form or arnassi armor""" + locations = [ + "Mithalas City Castle, beating the Priests", + "Arnassi Ruins, Crab Armor", + "Arnassi Ruins, Song Plant Spore", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Castle flower tube entrance", + "Mermog cave, Piranha Egg", + "Mithalas Cathedral, Mithalan Dress", + "Kelp Forest top left area, Jelly Egg", + "The Veil top right area, bulb in the middle of the wall jump cliff", + "The Veil top right area, bulb at the top of the waterfall", + "Sunken City, bulb on top of the boss area", + "Octopus Cave, Dumbo Egg", + "Beating the Golem", + "Beating Mergog", + "Beating Crabbius Maximus", + "Beating Octopus Prime", + "Beating Mithalan priests", + "Sunken City cleared" + ] + items = [["Beast form", "Arnassi Armor"]] + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index 82d8e89a0066..b443166823bc 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -17,55 +17,16 @@ class EnergyFormAccessTest(AquariaTestBase): def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Home Water, Nautilus Egg", - "Naija's Home, bulb after the energy door", - "Energy Temple first area, bulb in the bottom room blocked by a rock", "Energy Temple second area, bulb under the rock", - "Energy Temple bottom entrance, Krotite Armor", "Energy Temple third area, bulb in the bottom path", - "Energy Temple boss area, Fallen God Tooth", - "Energy Temple blaster room, Blaster Egg", - "Mithalas City Castle, beating the Priests", - "Mithalas Cathedral, first urn in the top right room", - "Mithalas Cathedral, second urn in the top right room", - "Mithalas Cathedral, third urn in the top right room", - "Mithalas Cathedral, urn in the flesh room with fleas", - "Mithalas Cathedral, first urn in the bottom right path", - "Mithalas Cathedral, second urn in the bottom right path", - "Mithalas Cathedral, urn behind the flesh vein", - "Mithalas Cathedral, urn in the top left eyes boss room", - "Mithalas Cathedral, first urn in the path behind the flesh vein", - "Mithalas Cathedral, second urn in the path behind the flesh vein", - "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, 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", - "Mermog cave, Piranha Egg", - "Octopus Cave, Dumbo Egg", - "Sun Temple boss area, beating Sun God", - "Arnassi Ruins, Crab Armor", - "King Jellyfish Cave, bulb in the right path from King Jelly", - "King Jellyfish Cave, Jellyfish Costume", - "Sunken City, bulb on top of the boss area", + "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", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", "Final Boss area, bulb in the boss third form room", - "Beating Fallen God", - "Beating Mithalan God", - "Beating Drunian God", - "Beating Sun God", - "Beating the Golem", - "Beating Nautilus Prime", - "Beating Blaster Peg Prime", - "Beating Mergog", - "Beating Mithalan priests", - "Beating Octopus Prime", - "Beating Crabbius Maximus", - "Beating King Jellyfish God Prime", - "First secret", - "Sunken City cleared", "Objective complete", ] items = [["Energy form"]] diff --git a/worlds/aquaria/test/test_energy_form_or_dual_form_access.py b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py new file mode 100644 index 000000000000..8a765bc4e4e2 --- /dev/null +++ b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py @@ -0,0 +1,92 @@ +""" +Author: Louis M +Date: Thu, 18 Apr 2024 18:45:56 +0000 +Description: Unit test used to test accessibility of locations with and without the energy form and dual form (and Li) +""" + +from . import AquariaTestBase + + +class EnergyFormDualFormAccessTest(AquariaTestBase): + """Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)""" + options = { + "early_energy_form": False, + } + + def test_energy_form_or_dual_form_location(self) -> None: + """Test locations that require Energy form or dual form""" + locations = [ + "Naija's Home, bulb after the energy door", + "Home Water, Nautilus Egg", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple blaster room, Blaster Egg", + "Energy Temple boss area, Fallen God Tooth", + "Mithalas City Castle, beating the Priests", + "Mithalas boss area, beating Mithalan God", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "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, urn below the left entrance", + "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", + "Mermog cave, Piranha Egg", + "Octopus Cave, Dumbo Egg", + "Sun Temple boss area, beating Sun God", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "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 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", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", + "Final Boss area, first bulb in the turtle room", + "Final Boss area, second bulb in the turtle room", + "Final Boss area, third bulb in the turtle room", + "Final Boss area, Transturtle", + "Beating Fallen God", + "Beating Blaster Peg Prime", + "Beating Mithalan God", + "Beating Drunian God", + "Beating Sun God", + "Beating the Golem", + "Beating Nautilus Prime", + "Beating Mergog", + "Beating Mithalan priests", + "Beating Octopus Prime", + "Beating King Jellyfish God Prime", + "Beating the Golem", + "Sunken City cleared", + "First secret", + "Objective complete" + ] + items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]] + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index c98a53e92438..40b15a87cd35 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -17,6 +17,7 @@ def test_fish_form_location(self) -> None: """Test locations that require fish form""" locations = [ "The Veil top left area, bulb inside the fish pass", + "Energy Temple first area, Energy Idol", "Mithalas City, Doll", "Mithalas City, urn inside a home fish pass", "Kelp Forest top right area, bulb in the top fish pass", @@ -30,8 +31,7 @@ def test_fish_form_location(self) -> None: "Octopus Cave, Dumbo Egg", "Octopus Cave, bulb in the path below the Octopus Cave path", "Beating Octopus Prime", - "Abyss left area, bulb in the bottom fish pass", - "Arnassi Ruins, Arnassi Armor" + "Abyss left area, bulb in the bottom fish pass" ] items = [["Fish form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index b5d7cf99fea2..29d37d790b20 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -39,7 +39,6 @@ def test_light_location(self) -> None: "Abyss right area, bulb in the middle path", "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", - "Abyss right area, Transturtle", "Ice Cave, bulb in the room to the right", "Ice Cave, first bulb in the top exit room", "Ice Cave, second bulb in the top exit room", diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 3bcbd7d72e02..7e31de9905e9 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -30,7 +30,6 @@ def test_spirit_form_location(self) -> None: "Sunken City left area, Girl Costume", "Beating Mantis Shrimp Prime", "First secret", - "Arnassi Ruins, Arnassi Armor", ] items = [["Spirit form"]] self.assertAccessDependency(locations, items) From 6803c373e5ff738914c362b5e7a158fd528f54f7 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 8 Aug 2024 13:33:13 -0500 Subject: [PATCH 125/393] HK: add grub hunt goal (#3203) * makes grub hunt goal option that calculates the total available grubs (including item link replacements) and requires all of them to be gathered for goal completion * update slot data name for grub count * add option to set number needed for grub hub * updates to grub hunt goal based on review * copy/paste fix * account for 'any' goal and fix overriding non-grub goals * making sure godhome is in logic for any and removing redundancy on completion condition * fix typing * i hate typing * move to stage_pre_fill * modify "any" goal so all goals are in logic under minimal settings * rewrite grub counting to create lookups for grubs and groups that can be reused * use generator instead of list comprehension * fix whitespace merging wrong * minor code cleanup --- worlds/hk/Options.py | 13 ++++++++- worlds/hk/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index e2602036a24e..c1206d41ee2c 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -405,9 +405,20 @@ class Goal(Choice): option_radiance = 3 option_godhome = 4 option_godhome_flower = 5 + option_grub_hunt = 6 default = 0 +class GrubHuntGoal(NamedRange): + """The amount of grubs required to finish Grub Hunt. + On 'All' any grubs from item links replacements etc. will be counted""" + display_name = "Grub Hunt Goal" + range_start = 1 + range_end = 46 + special_range_names = {"all": -1} + default = 46 + + class WhitePalace(Choice): """ Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be @@ -522,7 +533,7 @@ class CostSanityHybridChance(Range): **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, + StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index e5065876ddf3..99277378a162 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,6 +5,7 @@ from copy import deepcopy import itertools import operator +from collections import defaultdict, Counter logger = logging.getLogger("Hollow Knight") @@ -12,12 +13,12 @@ from .Regions import create_regions from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option, HKOptions + shop_to_option, HKOptions, GrubHuntGoal from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification +from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld path_of_pain_locations = { @@ -155,6 +156,7 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} + grub_count: int def __init__(self, multiworld, player): super(HKWorld, self).__init__(multiworld, player) @@ -164,6 +166,7 @@ def __init__(self, multiworld, player): self.ranges = {} self.created_shop_items = 0 self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) + self.grub_count = 0 def generate_early(self): options = self.options @@ -201,7 +204,7 @@ def create_regions(self): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower, Goal.option_any]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -441,12 +444,67 @@ def set_rules(self): multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + elif goal == Goal.option_grub_hunt: + pass # will set in stage_pre_fill() else: # Any goal - multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \ + _hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) set_rules(self) + @classmethod + def stage_pre_fill(cls, multiworld: "MultiWorld"): + def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): + world = multiworld.worlds[player] + + if world.options.Goal == "grub_hunt": + multiworld.completion_condition[player] = grub_rule + else: + old_rule = multiworld.completion_condition[player] + multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state) + + worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] + if worlds: + grubs = [item for item in multiworld.get_items() if item.name == "Grub"] + all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + + if all_grub_players: + group_lookup = defaultdict(set) + for group_id, group in multiworld.groups.items(): + for player in group["players"]: + group_lookup[group_id].add(player) + + grub_count_per_player = Counter() + per_player_grubs_per_player = defaultdict(Counter) + + for grub in grubs: + player = grub.player + if player in group_lookup: + for real_player in group_lookup[player]: + per_player_grubs_per_player[real_player][player] += 1 + else: + per_player_grubs_per_player[player][player] += 1 + + if grub.location and grub.location.player in group_lookup.keys(): + for real_player in group_lookup[grub.location.player]: + grub_count_per_player[real_player] += 1 + else: + grub_count_per_player[player] += 1 + + for player, count in grub_count_per_player.items(): + multiworld.worlds[player].grub_count = count + + for player, grub_player_count in per_player_grubs_per_player.items(): + if player in all_grub_players: + set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items())) + + for world in worlds: + if world.player not in all_grub_players: + world.grub_count = world.options.GrubHuntGoal.value + player = world.player + set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c)) + def fill_slot_data(self): slot_data = {} @@ -484,6 +542,8 @@ def fill_slot_data(self): slot_data["notch_costs"] = self.charm_costs + slot_data["grub_count"] = self.grub_count + return slot_data def create_item(self, name: str) -> HKItem: From 5efb3fd2b0450f68dc95f3b79a0f48746b5e732d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 9 Aug 2024 03:14:26 -0700 Subject: [PATCH 126/393] DS3: Version 3.0.0 (#3128) * Update worlds/dark_souls_3/Locations.py Co-authored-by: Scipio Wright * Fix Covetous Silver Serpent Ring location * Update location groups This should cover pretty much all of the seriously hidden items. It also splits out miniboss drops, mimic drops, and hostile NPC drops. * Remove the "Guarded by Keys" group On reflection, I don't think this is actually that useful. It'll also get a lot muddier once we can randomize shops and ashes become pseudo-"keys". * Restore Knight Slayer's Ring classification * Support infusions/upgrades in the new DS3 mod system * Support random starting loadouts * Make an item's NPC status orthogonal to its category * Track location groups with flags * Track Archipelago/Offline mismatches on the server Also fix a few incorrect item names. * Add additional locations that are now randomizable * Don't put soul and multiple items in shops * Add an option to enable whether NG+ items/locations are included * Clean up useful item categorization There are so many weapons in the game now, it doesn't make sense to treat them all as useful * Add more variety to filler items * Iron out a few bugs and incompatibilities * Fix more silly bugs * Get tests passing * Update options to cover new item types Also recategorize some items. * Verify the default values of `Option`s. Since `Option.verify()` can handle normalization of option names, this allows options to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default. This also makes it easier to catch errors if a world author accidentally sets an invalid default. * Make a few more improvements and fixes * Randomize Path of the Dragon * Mark items that unlock checks as useful These items all unlock missable checks, but they're still good to ahve in the game for variety's sake. * Guarantee more NPC quests are completable * Fix a syntax error * Fix rule definition * Support enemy randomization * Support online Yhorm randomization * Remove a completed TODO * Fix tests * Fix force_unique * Add an option to smooth out upgrade item progression * Add helpers for setting location/entrance rules * Support smoother soul item progression * Fill extra smoothing items into conditional locations as well as other worlds * Add health item smoothing * Handle infusions at item generation time * Handle item upgrades at genreation time * Fix Grave Warden's Ashes * Don't overwrite old rules * Randomize items based on spheres instead of DS3 locations * Add a smoothing option for weapon upgrades * Add rules for crow trades * Small fixes * Fix a few more bugs * Fix more bugs * Try to prevent Path of the Dragon from going somewhere it doesn't work * Add the ability to provide enemy presets * Various fixes and features * Bug fixes * Better Coiled Sword placement * Structure DarkSouls3Location more like DarkSouls3Item * Add events to make DS3's spheres more even * Restructure locations to work like items do now * Add rules for more missable locations * Don't add two Storm Rulers * Place Hawk Ring in Farron Keep * Mark the Grass Crest Shield as useful * Mark new progression items * Fix a bug * Support newer better Path of the Dragon code * Don't lock the player out of Coiled Sword * Don't create events for missable locations * Don't throw strings * Don't smooth event items * Properly categorize Butcher Knife * Be more careful about placing Yhorm in low-randomization scenarios * Don't try to smooth DLC items with DLC disabled * Fix another Yhorm bug * Fix upgrade/infusion logic * Remove the PoolType option This distinction is no longer meaningful now that every location in the game of each type is randomized * Categorize HWL: Red Eye Orb as an NPC location * Don't place Storm Ruler on CA: Coiled Sword * Define flatten() locally to make this APWorld capable * Fix some more Leonhard weirdness * Fix unique item randomization * Don't double Twin Dragon Greatshield * Remove debugging print * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Add a missing dlc_enabled check * Use nicer options syntax * Bump data_version * Mention where Yhorm is in which world * Better handle excluded events * Add a newline to Yhorm location * Better way of handling excluded unradomized progression locations * Fix a squidge of nondeterminism * Only smooth items from this world * Don't smooth progression weapons * Remove a location that doesn't actually exist in-game * Classify Power Within as useful * Clarify location names * Fix location requirements * Clean up randomization options * Properly name Coiled Sword location * Add an option for configuring how missable items are handled * Fix some bugs from location name updates * Fix location guide link * Fix a couple locations that were busted offline * Update detailed location descriptions * Fix some bugs when generating for a multiworld * Inject Large Leather Shield * Fix a few location issues * Don't allow progression_skip_balancing for unnecessary locs * Update some location info * Don't uniquify the wrong items * Fix some more location issues * More location fixes * Use hyphens instead of parens for location descriptions * Update and fix more locations * Fix Soul of Cinder boss name * Fix some logic issues * Add item groups and document item/location groups * Fix the display name for "Impatient Mimics" * Properly handle Transposing Kiln and Pyromancer's Flame * Testing * Some fixes to NPC quests, late basin, and transposing kiln * Improve a couple location names * Split out and improve missable NPC item logic * Don't allow crow trades to have foreign items * Fix a variable capture bug * Make sure early items are accessible early even with early Castle * Mark ID giant slave drops as missable * Make sure late basin means that early items aren't behind it * Make is_location_available explicitly private * Add an _add_item_rule utility that checks availability * Clear excluded items if excluded_locations == "unnecessary" * Don't allow upgrades/infusions in crow trades * Fix the documentation for deprecated options * Create events for all excluded locations This allows `can_reach` logic to work even if the locations are randomized. * Fix up Patches' and Siegward's logic based on some manual testing * Factor out more sub-methods for setting location rules * Oops, left these in * Fixing name * Left that in too * Changing to NamedRange to support special_range_names * Alphabetizing * Don't call _is_location_available on foreign locations * Add missing Leonhard items * Changing late basin to have a post-small-doll option * Update basin option, add logic for some of Leonhard Hawkwood and Orbeck * Simplifying an option, fixing a copy-paste error * Removing trailing whitespace * Changing lost items to go into start inventory * Revert Basin changes * Oops * Update Options.py * Reverting small doll changes * Farron Keep boss requirement logic * Add Scroll for late_dlc * Fixing excluded unnecessary locations * Adding Priestess Ring as being after UG boss * Removing missable from Corvian Titanite Slab * Adding KFF Yhorm boss locks * Screams about Creighton * Elite Knight Set isn't permanently missable * Adding Kiln requirement to KFF * fixing valid_keys and item groups * Fixing an option-checker * Throwing unplaceable Storm Ruler into start inventory * Update locations * Refactor item injection * Update setup doc * Small fixes * Fix another location name * Fix injection calculation * Inject guaranteed items along with progression items * Mark boss souls as required for access to regions This allows us to set quest requirements for boss souls and have them automatically propagated to regions, means we need less machinery for Yhorm bosses, and allows us to get rid of a few region-transition events. * Make sure Sirris's quest can be completed before Pontiff * Removing unused list * Changing dict to list * Removing unused test * Update __init__.py * self.multiworld.random -> self.random (#9) * Fix some miscellaneous location issues * Rewrite the DS3 intro page/FAQ * Removing modifying the itempool after fill (#7) Co-authored-by: Natalie Weizenbaum * Small fixes to the setup guide (#10) Small fixes, adding an example for connecting * Expanded Late Basin of Vows and Late DLC (#6) * Add proper requirements for CD: Black Eye Orb * Fix Aldrich's name * Document the differences with the 2.x.x branch * Don't crash if there are more items than locations in smoothing * Apply suggestions from code review Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Code review * Fix _replace_with_filler * Don't use the shared flatten function in SM * Track local items separately rather than iterating the multiworld * Various formatting/docs changes suggested by PyCharm (#12) * Drop deprecated options * Rename "offline randomizer" to "static randomizer" which is clearer * Move `enable_*_locations` under removed options. * Avoid excluded locations for locally-filled items * Adding Removed options to error (#14) * Changes for WebHost options display and the options overhaul * unpack iterators in item list (#13) * Allow worlds to add options to prebuilt groups Previously, this crashed because `typing.NamedTuple` fields such as `group.name` aren't assignable. Now it will only fail for group names that are actually incorrectly cased, and will fail with a better error message. * Style changes, rename exclude behavior options, remove guaranteed items option * Spacing/Formatting (#18) * Various Fixes (#19) * Universally Track Yhorm (#20) * Account for excluded and missable * These are behaviors now * This is singular, apparently * Oops * Fleshing out the priority process * Missable Titanite Lizards and excluded locations (#22) * Small style/efficiency changes * Final passthrough fixes (#24) * Use rich option formatting * Make the behavior option values actual behaviors (#25) * Use != * Remove unused flatten utility * Some changes from review (#28) * Fixing determinism and making smooth faster (#29) * Style change * PyCharm and Mypy fixes (#26) Co-authored-by: Scipio Wright * Change yhorm default (#30) * Add indirect condition (#27) * Update worlds/dark_souls_3/docs/locations_en.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Ship all item IDs to the client This avoids issues where items might get skipped if, for instance, they're only in the starting inventory. * Make sure to send AP IDs for infused/upgraded weapons * Make `RandomEnemyPresetOption` compatible with ArchipelagoMW/Archipelago#3280 (#31) * Fix cast * More typing and small fixes (#32) --------- Co-authored-by: Scipio Wright Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Doug Hoskisson Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/dark_souls_3/Bosses.py | 264 ++ worlds/dark_souls_3/Items.py | 2812 +++++++------ worlds/dark_souls_3/Locations.py | 3686 ++++++++++++++--- worlds/dark_souls_3/Options.py | 507 ++- worlds/dark_souls_3/__init__.py | 1785 ++++++-- .../detailed_location_descriptions.py | 97 + worlds/dark_souls_3/docs/en_Dark Souls III.md | 203 +- worlds/dark_souls_3/docs/items_en.md | 24 + worlds/dark_souls_3/docs/locations_en.md | 2276 ++++++++++ worlds/dark_souls_3/docs/setup_en.md | 61 +- worlds/dark_souls_3/test/TestDarkSouls3.py | 27 + worlds/dark_souls_3/test/__init__.py | 0 12 files changed, 9354 insertions(+), 2388 deletions(-) create mode 100644 worlds/dark_souls_3/Bosses.py create mode 100644 worlds/dark_souls_3/detailed_location_descriptions.py create mode 100644 worlds/dark_souls_3/docs/items_en.md create mode 100644 worlds/dark_souls_3/docs/locations_en.md create mode 100644 worlds/dark_souls_3/test/TestDarkSouls3.py create mode 100644 worlds/dark_souls_3/test/__init__.py diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py new file mode 100644 index 000000000000..008a29713202 --- /dev/null +++ b/worlds/dark_souls_3/Bosses.py @@ -0,0 +1,264 @@ +# In almost all cases, we leave boss and enemy randomization up to the static randomizer. But for +# Yhorm specifically we need to know where he ends up in order to ensure that the Storm Ruler is +# available before his fight. + +from dataclasses import dataclass, field +from typing import Set + + +@dataclass +class DS3BossInfo: + """The set of locations a given boss location blocks access to.""" + + name: str + """The boss's name.""" + + id: int + """The game's ID for this particular boss.""" + + dlc: bool = False + """This boss appears in one of the game's DLCs.""" + + before_storm_ruler: bool = False + """Whether this location appears before it's possible to get Storm Ruler in vanilla. + + This is used to determine whether it's safe to place Yhorm here if weapons + aren't randomized. + """ + + locations: Set[str] = field(default_factory=set) + """Additional individual locations that can't be accessed until the boss is dead.""" + + +# Note: the static randomizer splits up some bosses into separate fights for separate phases, each +# of which can be individually replaced by Yhorm. +all_bosses = [ + DS3BossInfo("Iudex Gundyr", 4000800, before_storm_ruler = True, locations = { + "CA: Coiled Sword - boss drop" + }), + DS3BossInfo("Vordt of the Boreal Valley", 3000800, before_storm_ruler = True, locations = { + "HWL: Soul of Boreal Valley Vordt" + }), + DS3BossInfo("Curse-rotted Greatwood", 3100800, locations = { + "US: Soul of the Rotted Greatwood", + "US: Transposing Kiln - boss drop", + "US: Wargod Wooden Shield - Pit of Hollows", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + "FS: Sunless Talisman - Sirris, kill GA boss", + "FS: Sunless Veil - shop, Sirris quest, kill GA boss", + "FS: Sunless Armor - shop, Sirris quest, kill GA boss", + "FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + }), + DS3BossInfo("Crystal Sage", 3300850, locations = { + "RS: Soul of a Crystal Sage", + "FS: Sage's Big Hat - shop after killing RS boss", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("Deacons of the Deep", 3500800, locations = { + "CD: Soul of the Deacons of the Deep", + "CD: Small Doll - boss drop", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { + "FK: Soul of the Blood of the Wolf", + "FK: Cinders of a Lord - Abyss Watcher", + "FS: Undead Legion Helm - shop after killing FK boss", + "FS: Undead Legion Armor - shop after killing FK boss", + "FS: Undead Legion Gauntlet - shop after killing FK boss", + "FS: Undead Legion Leggings - shop after killing FK boss", + "FS: Farron Ring - Hawkwood", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("High Lord Wolnir", 3800800, before_storm_ruler = True, locations = { + "CC: Soul of High Lord Wolnir", + "FS: Wolnir's Crown - shop after killing CC boss", + "CC: Homeward Bone - Irithyll bridge", + "CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop", + }), + DS3BossInfo("Pontiff Sulyvahn", 3700850, locations = { + "IBV: Soul of Pontiff Sulyvahn", + }), + DS3BossInfo("Old Demon King", 3800830, locations = { + "SL: Soul of the Old Demon King", + }), + DS3BossInfo("Aldrich, Devourer of Gods", 3700800, locations = { + "AL: Soul of Aldrich", + "AL: Cinders of a Lord - Aldrich", + "FS: Smough's Helm - shop after killing AL boss", + "FS: Smough's Armor - shop after killing AL boss", + "FS: Smough's Gauntlets - shop after killing AL boss", + "FS: Smough's Leggings - shop after killing AL boss", + "AL: Sun Princess Ring - dark cathedral, after boss", + "FS: Leonhard's Garb - shop after killing Leonhard", + "FS: Leonhard's Gauntlets - shop after killing Leonhard", + "FS: Leonhard's Trousers - shop after killing Leonhard", + }), + DS3BossInfo("Dancer of the Boreal Valley", 3000899, locations = { + "HWL: Soul of the Dancer", + "FS: Dancer's Crown - shop after killing LC entry boss", + "FS: Dancer's Armor - shop after killing LC entry boss", + "FS: Dancer's Gauntlets - shop after killing LC entry boss", + "FS: Dancer's Leggings - shop after killing LC entry boss", + }), + DS3BossInfo("Dragonslayer Armour", 3010800, locations = { + "LC: Soul of Dragonslayer Armour", + "FS: Morne's Helm - shop after killing Eygon or LC boss", + "FS: Morne's Armor - shop after killing Eygon or LC boss", + "FS: Morne's Gauntlets - shop after killing Eygon or LC boss", + "FS: Morne's Leggings - shop after killing Eygon or LC boss", + "LC: Titanite Chunk - down stairs after boss", + }), + DS3BossInfo("Consumed King Oceiros", 3000830, locations = { + "CKG: Soul of Consumed Oceiros", + "CKG: Titanite Scale - tomb, chest #1", + "CKG: Titanite Scale - tomb, chest #2", + "CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + }), + DS3BossInfo("Champion Gundyr", 4000830, locations = { + "UG: Soul of Champion Gundyr", + "FS: Gundyr's Helm - shop after killing UG boss", + "FS: Gundyr's Armor - shop after killing UG boss", + "FS: Gundyr's Gauntlets - shop after killing UG boss", + "FS: Gundyr's Leggings - shop after killing UG boss", + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Chaos Blade - environs, left of shrine", + "UG: Blacksmith Hammer - shrine, Andre's room", + "UG: Eyes of a Fire Keeper - shrine, Irina's room", + "UG: Coiled Sword Fragment - shrine, dead bonfire", + "UG: Soul of a Crestfallen Knight - environs, above shrine entrance", + "UG: Life Ring+3 - shrine, behind big throne", + "UG: Ring of Steel Protection+1 - environs, behind bell tower", + "FS: Ring of Sacrifice - Yuria shop", + "UG: Ember - shop", + "UG: Priestess Ring - shop", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss", + }), + DS3BossInfo("Ancient Wyvern", 3200800), + DS3BossInfo("King of the Storm", 3200850, locations = { + "AP: Soul of the Nameless King", + "FS: Golden Crown - shop after killing AP boss", + "FS: Dragonscale Armor - shop after killing AP boss", + "FS: Golden Bracelets - shop after killing AP boss", + "FS: Dragonscale Waistcloth - shop after killing AP boss", + "AP: Titanite Slab - plaza", + "AP: Covetous Gold Serpent Ring+2 - plaza", + "AP: Dragonslayer Helm - plaza", + "AP: Dragonslayer Armor - plaza", + "AP: Dragonslayer Gauntlets - plaza", + "AP: Dragonslayer Leggings - plaza", + }), + DS3BossInfo("Nameless King", 3200851, locations = { + "AP: Soul of the Nameless King", + "FS: Golden Crown - shop after killing AP boss", + "FS: Dragonscale Armor - shop after killing AP boss", + "FS: Golden Bracelets - shop after killing AP boss", + "FS: Dragonscale Waistcloth - shop after killing AP boss", + "AP: Titanite Slab - plaza", + "AP: Covetous Gold Serpent Ring+2 - plaza", + "AP: Dragonslayer Helm - plaza", + "AP: Dragonslayer Armor - plaza", + "AP: Dragonslayer Gauntlets - plaza", + "AP: Dragonslayer Leggings - plaza", + }), + DS3BossInfo("Lothric, Younger Prince", 3410830, locations = { + "GA: Soul of the Twin Princes", + "GA: Cinders of a Lord - Lothric Prince", + }), + DS3BossInfo("Lorian, Elder Prince", 3410832, locations = { + "GA: Soul of the Twin Princes", + "GA: Cinders of a Lord - Lothric Prince", + "FS: Lorian's Helm - shop after killing GA boss", + "FS: Lorian's Armor - shop after killing GA boss", + "FS: Lorian's Gauntlets - shop after killing GA boss", + "FS: Lorian's Leggings - shop after killing GA boss", + }), + DS3BossInfo("Champion's Gravetender and Gravetender Greatwolf", 4500860, dlc = True, + locations = {"PW1: Valorheart - boss drop"}), + DS3BossInfo("Sister Friede", 4500801, dlc = True, locations = { + "PW2: Soul of Sister Friede", + "PW2: Titanite Slab - boss drop", + "PW1: Titanite Slab - Corvian", + "FS: Ordained Hood - shop after killing PW2 boss", + "FS: Ordained Dress - shop after killing PW2 boss", + "FS: Ordained Trousers - shop after killing PW2 boss", + }), + DS3BossInfo("Blackflame Friede", 4500800, dlc = True, locations = { + "PW2: Soul of Sister Friede", + "PW1: Titanite Slab - Corvian", + "FS: Ordained Hood - shop after killing PW2 boss", + "FS: Ordained Dress - shop after killing PW2 boss", + "FS: Ordained Trousers - shop after killing PW2 boss", + }), + DS3BossInfo("Demon Prince", 5000801, dlc = True, locations = { + "DH: Soul of the Demon Prince", + "DH: Small Envoy Banner - boss drop", + }), + DS3BossInfo("Halflight, Spear of the Church", 5100800, dlc = True, locations = { + "RC: Titanite Slab - mid boss drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Titanite Slab - ashes, mob drop", + "RC: Filianore's Spear Ornament - mid boss drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Darkeater Midir", 5100850, dlc = True, locations = { + "RC: Soul of Darkeater Midir", + "RC: Spears of the Church - hidden boss drop", + }), + DS3BossInfo("Slave Knight Gael 1", 5110801, dlc = True, locations = { + "RC: Soul of Slave Knight Gael", + "RC: Blood of the Dark Soul - end boss drop", + # These are accessible before you trigger the boss, but once you do you + # have to beat it before getting them. + "RC: Titanite Slab - ashes, mob drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Sacred Chime of Filianore - ashes, NPC drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Slave Knight Gael 2", 5110800, dlc = True, locations = { + "RC: Soul of Slave Knight Gael", + "RC: Blood of the Dark Soul - end boss drop", + # These are accessible before you trigger the boss, but once you do you + # have to beat it before getting them. + "RC: Titanite Slab - ashes, mob drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Sacred Chime of Filianore - ashes, NPC drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Lords of Cinder", 4100800, locations = { + "KFF: Soul of the Lords", + "FS: Billed Mask - Yuria after killing KFF boss", + "FS: Black Dress - Yuria after killing KFF boss", + "FS: Black Gauntlets - Yuria after killing KFF boss", + "FS: Black Leggings - Yuria after killing KFF boss" + }), +] + +default_yhorm_location = DS3BossInfo("Yhorm the Giant", 3900800, locations = { + "PC: Soul of Yhorm the Giant", + "PC: Cinders of a Lord - Yhorm the Giant", + "PC: Siegbräu - Siegward after killing boss", +}) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index 3dd5cb2d3c3f..19cd79a99414 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -1,7 +1,9 @@ +from dataclasses import dataclass +import dataclasses from enum import IntEnum -from typing import NamedTuple +from typing import Any, cast, ClassVar, Dict, Generator, List, Optional, Set -from BaseClasses import Item +from BaseClasses import Item, ItemClassification class DS3ItemCategory(IntEnum): @@ -14,1267 +16,1677 @@ class DS3ItemCategory(IntEnum): RING = 6 SPELL = 7 MISC = 8 - KEY = 9 + UNIQUE = 9 BOSS = 10 - SKIP = 11 + SOUL = 11 + UPGRADE = 12 + HEALING = 13 + @property + def is_infusible(self) -> bool: + """Returns whether this category can be infused.""" + return self in [ + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, + DS3ItemCategory.SHIELD_INFUSIBLE + ] + + @property + def upgrade_level(self) -> Optional[int]: + """The maximum upgrade level for this category, or None if it's not upgradable.""" + if self == DS3ItemCategory.WEAPON_UPGRADE_5: return 5 + if self in [ + DS3ItemCategory.WEAPON_UPGRADE_10, + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE + ]: return 10 + return None + + +@dataclass +class Infusion(IntEnum): + """Infusions supported by Dark Souls III. + + The value of each infusion is the number added to the base weapon's ID to get the infused ID. + """ + + HEAVY = 100 + SHARP = 200 + REFINED = 300 + SIMPLE = 400 + CRYSTAL = 500 + FIRE = 600 + CHAOS = 700 + LIGHTNING = 800 + DEEP = 900 + DARK = 1000 + POISON = 1100 + BLOOD = 1200 + RAW = 1300 + BLESSED = 1400 + HOLLOW = 1500 + + @property + def prefix(self): + """The prefix to add to a weapon name with this infusion.""" + return self.name.title() + + +class UsefulIf(IntEnum): + """An enum that indicates when an item should be upgraded to ItemClassification.useful. + + This is used for rings with +x variants that may or may not be the best in class depending on + the player's settings. + """ + + DEFAULT = 0 + """Follows DS3ItemData.classification as written.""" + + BASE = 1 + """Useful only if the DLC and NG+ locations are disabled.""" + + NO_DLC = 2 + """Useful if the DLC is disabled, whether or not NG+ locations are.""" + + NO_NGP = 3 + """Useful if NG+ locations is disabled, whether or not the DLC is.""" + + +@dataclass +class DS3ItemData: + __item_id: ClassVar[int] = 100000 + """The next item ID to use when creating item data.""" -class DS3ItemData(NamedTuple): name: str - ds3_code: int - is_dlc: bool + ds3_code: Optional[int] category: DS3ItemCategory + base_ds3_code: Optional[int] = None + """If this is an upgradable weapon, the base ID of the weapon it upgrades from. + + Otherwise, or if the weapon isn't upgraded, this is the same as ds3_code. + """ + + base_name: Optional[str] = None + """The name of the individual item, if this is a multi-item group.""" + + classification: ItemClassification = ItemClassification.filler + """How important this item is to the game progression.""" + + ap_code: Optional[int] = None + """The Archipelago ID for this item.""" + + is_dlc: bool = False + """Whether this item is only found in one of the two DLC packs.""" + + count: int = 1 + """The number of copies of this item included in each drop.""" + + inject: bool = False + """If this is set, the randomizer will try to inject this item into the game. + + This is used for items such as covenant rewards that aren't realistically reachable in a + randomizer run, but are still fun to have available to the player. If there are more locations + available than there are items in the item pool, these items will be used to help make up the + difference. + """ + + souls: Optional[int] = None + """If this is a consumable item that gives souls, the number of souls it gives.""" + + useful_if: UsefulIf = UsefulIf.DEFAULT + """Whether and when this item should be marked as "useful".""" + + filler: bool = False + """Whether this is a candidate for a filler item to be added to fill out extra locations.""" + + skip: bool = False + """Whether to omit this item from randomization and replace it with filler or unique items.""" + + @property + def unique(self): + """Whether this item should be unique, appearing only once in the randomizer.""" + return self.category not in { + DS3ItemCategory.MISC, DS3ItemCategory.SOUL, DS3ItemCategory.UPGRADE, + DS3ItemCategory.HEALING, + } + + def __post_init__(self): + self.ap_code = self.ap_code or DS3ItemData.__item_id + if not self.base_name: self.base_name = self.name + if not self.base_ds3_code: self.base_ds3_code = self.ds3_code + DS3ItemData.__item_id += 1 + + def item_groups(self) -> List[str]: + """The names of item groups this item should appear in. + + This is computed from the properties assigned to this item.""" + names = [] + if self.classification == ItemClassification.progression: names.append("Progression") + if self.name.startswith("Cinders of a Lord -"): names.append("Cinders") + + names.append({ + DS3ItemCategory.WEAPON_UPGRADE_5: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE: "Weapons", + DS3ItemCategory.SHIELD: "Shields", + DS3ItemCategory.SHIELD_INFUSIBLE: "Shields", + DS3ItemCategory.ARMOR: "Armor", + DS3ItemCategory.RING: "Rings", + DS3ItemCategory.SPELL: "Spells", + DS3ItemCategory.MISC: "Miscellaneous", + DS3ItemCategory.UNIQUE: "Unique", + DS3ItemCategory.BOSS: "Boss Souls", + DS3ItemCategory.SOUL: "Small Souls", + DS3ItemCategory.UPGRADE: "Upgrade", + DS3ItemCategory.HEALING: "Healing", + }[self.category]) + + return names + + def counts(self, counts: List[int]) -> Generator["DS3ItemData", None, None]: + """Returns an iterable of copies of this item with the given counts.""" + yield self + for count in counts: + yield dataclasses.replace( + self, + ap_code = None, + name = "{} x{}".format(self.base_name, count), + base_name = self.base_name, + count = count, + filler = False, # Don't count multiples as filler by default + ) + + @property + def is_infused(self) -> bool: + """Returns whether this item is an infused weapon.""" + return cast(int, self.ds3_code) - cast(int, self.base_ds3_code) >= 100 + + def infuse(self, infusion: Infusion) -> "DS3ItemData": + """Returns this item with the given infusion applied.""" + if not self.category.is_infusible: raise RuntimeError(f"{self.name} is not infusible.") + if self.is_infused: + raise RuntimeError(f"{self.name} is already infused.") + + # We can't change the name or AP code when infusing/upgrading weapons, because they both + # need to match what's in item_name_to_id. We don't want to add every possible + # infusion/upgrade combination to that map because it's way too many items. + return dataclasses.replace( + self, + name = self.name, + ds3_code = cast(int, self.ds3_code) + infusion.value, + filler = False, + ) + + @property + def is_upgraded(self) -> bool: + """Returns whether this item is a weapon that's upgraded beyond level 0.""" + return (cast(int, self.ds3_code) - cast(int, self.base_ds3_code)) % 100 != 0 + + def upgrade(self, level: int) -> "DS3ItemData": + """Upgrades this item to the given level.""" + if not self.category.upgrade_level: raise RuntimeError(f"{self.name} is not upgradable.") + if level > self.category.upgrade_level: + raise RuntimeError(f"{self.name} can't be upgraded to +{level}.") + if self.is_upgraded: + raise RuntimeError(f"{self.name} is already upgraded.") + + # We can't change the name or AP code when infusing/upgrading weapons, because they both + # need to match what's in item_name_to_id. We don't want to add every possible + # infusion/upgrade combination to that map because it's way too many items. + return dataclasses.replace( + self, + name = self.name, + ds3_code = cast(int, self.ds3_code) + level, + filler = False, + ) + + def __hash__(self) -> int: + return (self.name, self.ds3_code).__hash__() + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self.name == other.name and self.ds3_code == other.ds3_code + else: + return False + class DarkSouls3Item(Item): game: str = "Dark Souls III" + data: DS3ItemData + + @property + def level(self) -> Optional[int]: + """This item's upgrade level, if it's a weapon.""" + return cast(int, self.data.ds3_code) % 100 if self.data.category.upgrade_level else None + + def __init__( + self, + player: int, + data: DS3ItemData, + classification = None): + super().__init__(data.name, classification or data.classification, data.ap_code, player) + self.data = data @staticmethod - def get_name_to_id() -> dict: - base_id = 100000 - return {item_data.name: id for id, item_data in enumerate(_all_items, base_id)} - - -key_item_names = { - "Small Lothric Banner", - "Basin of Vows", - "Small Doll", - "Storm Ruler", - "Grand Archives Key", - "Cinders of a Lord - Abyss Watcher", - "Cinders of a Lord - Yhorm the Giant", - "Cinders of a Lord - Aldrich", - "Cinders of a Lord - Lothric Prince", - "Mortician's Ashes", - "Cell Key", - #"Tower Key", #Not a relevant key item atm - "Jailbreaker's Key", - "Prisoner Chief's Ashes", - "Old Cell Key", - "Jailer's Key Ring", - "Contraption Key", - "Small Envoy Banner" -} + def event(name: str, player: int) -> "DarkSouls3Item": + data = DS3ItemData(name, None, DS3ItemCategory.MISC, + skip = True, classification = ItemClassification.progression) + data.ap_code = None + return DarkSouls3Item(player, data) -_vanilla_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ +_vanilla_items = [ # Ammunition - ("Standard Arrow", 0x00061A80, DS3ItemCategory.SKIP), - ("Fire Arrow", 0x00061AE4, DS3ItemCategory.SKIP), - ("Poison Arrow", 0x00061B48, DS3ItemCategory.SKIP), - ("Large Arrow", 0x00061BAC, DS3ItemCategory.SKIP), - ("Feather Arrow", 0x00061C10, DS3ItemCategory.SKIP), - ("Moonlight Arrow", 0x00061C74, DS3ItemCategory.SKIP), - ("Wood Arrow", 0x00061CD8, DS3ItemCategory.SKIP), - ("Dark Arrow", 0x00061D3C, DS3ItemCategory.SKIP), - ("Dragonslayer Greatarrow", 0x00062250, DS3ItemCategory.SKIP), - ("Dragonslayer Lightning Arrow", 0x00062318, DS3ItemCategory.SKIP), - ("Onislayer Greatarrow", 0x0006237C, DS3ItemCategory.SKIP), - ("Standard Bolt", 0x00062A20, DS3ItemCategory.SKIP), - ("Heavy Bolt", 0x00062A84, DS3ItemCategory.SKIP), - ("Sniper Bolt", 0x00062AE8, DS3ItemCategory.SKIP), - ("Wood Bolt", 0x00062B4C, DS3ItemCategory.SKIP), - ("Lightning Bolt", 0x00062BB0, DS3ItemCategory.SKIP), - ("Splintering Bolt", 0x00062C14, DS3ItemCategory.SKIP), - ("Exploding Bolt", 0x00062C78, DS3ItemCategory.SKIP), + *DS3ItemData("Standard Arrow", 0x00061A80, DS3ItemCategory.MISC).counts([12]), + DS3ItemData("Standard Arrow x8", 0x00061A80, DS3ItemCategory.MISC, count = 8, filler = True), + DS3ItemData("Fire Arrow", 0x00061AE4, DS3ItemCategory.MISC), + DS3ItemData("Fire Arrow x8", 0x00061AE4, DS3ItemCategory.MISC, count = 8, filler = True), + *DS3ItemData("Poison Arrow", 0x00061B48, DS3ItemCategory.MISC).counts([18]), + DS3ItemData("Poison Arrow x8", 0x00061B48, DS3ItemCategory.MISC, count = 8, filler = True), + DS3ItemData("Large Arrow", 0x00061BAC, DS3ItemCategory.MISC), + DS3ItemData("Feather Arrow", 0x00061C10, DS3ItemCategory.MISC), + *DS3ItemData("Moonlight Arrow", 0x00061C74, DS3ItemCategory.MISC).counts([6]), + DS3ItemData("Wood Arrow", 0x00061CD8, DS3ItemCategory.MISC), + DS3ItemData("Dark Arrow", 0x00061D3C, DS3ItemCategory.MISC), + *DS3ItemData("Dragonslayer Greatarrow", 0x00062250, DS3ItemCategory.MISC).counts([5]), + *DS3ItemData("Dragonslayer Lightning Arrow", 0x00062318, DS3ItemCategory.MISC).counts([10]), + *DS3ItemData("Onislayer Greatarrow", 0x0006237C, DS3ItemCategory.MISC).counts([8]), + DS3ItemData("Standard Bolt", 0x00062A20, DS3ItemCategory.MISC), + DS3ItemData("Heavy Bolt", 0x00062A84, DS3ItemCategory.MISC), + *DS3ItemData("Sniper Bolt", 0x00062AE8, DS3ItemCategory.MISC).counts([11]), + DS3ItemData("Wood Bolt", 0x00062B4C, DS3ItemCategory.MISC), + *DS3ItemData("Lightning Bolt", 0x00062BB0, DS3ItemCategory.MISC).counts([9]), + *DS3ItemData("Lightning Bolt", 0x00062BB0, DS3ItemCategory.MISC).counts([12]), + DS3ItemData("Splintering Bolt", 0x00062C14, DS3ItemCategory.MISC), + *DS3ItemData("Exploding Bolt", 0x00062C78, DS3ItemCategory.MISC).counts([6]), # Weapons - ("Dagger", 0x000F4240, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Bandit's Knife", 0x000F6950, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Parrying Dagger", 0x000F9060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rotten Ghru Dagger", 0x000FDE80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Harpe", 0x00102CA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Scholar's Candlestick", 0x001053B0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Tailbone Short Sword", 0x00107AC0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Corvian Greatknife", 0x0010A1D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Handmaid's Dagger", 0x00111700, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Shortsword", 0x001E8480, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Longsword", 0x001EAB90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Broadsword", 0x001ED2A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Broken Straight Sword", 0x001EF9B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Sword", 0x001F6EE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Sunlight Straight Sword", 0x00203230, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Rotten Ghru Curved Sword", 0x00205940, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Irithyll Straight Sword", 0x0020A760, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cleric's Candlestick", 0x0020F580, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Morion Blade", 0x002143A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Astora's Straight Sword", 0x002191C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Barbed Straight Sword", 0x0021B8D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Executioner's Greatsword", 0x0021DFE0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Anri's Straight Sword", 0x002206F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Estoc", 0x002DC6C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Mail Breaker", 0x002DEDD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rapier", 0x002E14E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ricard's Rapier", 0x002E3BF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crystal Sage's Rapier", 0x002E6300, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Irithyll Rapier", 0x002E8A10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Shotel", 0x003D3010, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Scimitar", 0x003D7E30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Falchion", 0x003DA540, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Carthus Curved Sword", 0x003DCC50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Carthus Curved Greatsword", 0x003DF360, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Pontiff Knight Curved Sword", 0x003E1A70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Storm Curved Sword", 0x003E4180, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Painting Guardian's Curved Sword", 0x003E6890, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crescent Moon Sword", 0x003E8FA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Carthus Shotel", 0x003EB6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Uchigatana", 0x004C4B40, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Washing Pole", 0x004C7250, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Chaos Blade", 0x004C9960, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Black Blade", 0x004CC070, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Bloodlust", 0x004CE780, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Darkdrift", 0x004D0E90, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Bastard Sword", 0x005B8D80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Claymore", 0x005BDBA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Zweihander", 0x005C29C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatsword", 0x005C50D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Astora Greatsword", 0x005C9EF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Murakumo", 0x005CC600, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Greatsword", 0x005D1420, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Flamberge", 0x005DB060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Exile Greatsword", 0x005DD770, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatsword of Judgment", 0x005E2590, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Profaned Greatsword", 0x005E4CA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cathedral Knight Greatsword", 0x005E73B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Farron Greatsword", 0x005E9AC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Yhorm's Great Machete", 0x005F0FF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Dark Sword", 0x005F3700, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Sword", 0x005F5E10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lorian's Greatsword", 0x005F8520, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Twin Princes' Greatsword", 0x005FAC30, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lothric's Holy Sword", 0x005FD340, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Wolnir's Holy Sword", 0x005FFA50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Wolf Knight's Greatsword", 0x00602160, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Greatsword of Artorias", 0x0060216A, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Hollowslayer Greatsword", 0x00604870, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Moonlight Greatsword", 0x00606F80, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Drakeblood Greatsword", 0x00609690, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Firelink Greatsword", 0x0060BDA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Fume Ultra Greatsword", 0x0060E4B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Old Wolf Curved Sword", 0x00610BC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Storm Ruler", 0x006132D0, DS3ItemCategory.KEY), - ("Hand Axe", 0x006ACFC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Battle Axe", 0x006AF6D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Deep Battle Axe", 0x006AFA54, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Brigand Axe", 0x006B1DE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crescent Axe", 0x006B6C00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Axe", 0x006B9310, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Butcher Knife", 0x006BE130, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonslayer's Axe", 0x006C0840, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Thrall Axe", 0x006C5660, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragonslayer Greataxe", 0x006C7D70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Demon's Greataxe", 0x006CA480, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Eleonora", 0x006CCB90, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Man Serpent Hatchet", 0x006D19B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Club", 0x007A1200, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Mace", 0x007A3910, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Morning Star", 0x007A6020, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Reinforced Club", 0x007A8730, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Large Club", 0x007AFC60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Club", 0x007B4A80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Mace", 0x007BBFB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Wooden Hammer", 0x007C8300, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gargoyle Flame Hammer", 0x007CAA10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Vordt's Great Hammer", 0x007CD120, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Old King's Great Hammer", 0x007CF830, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Heysel Pick", 0x007D6D60, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Warpick", 0x007DBB80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Pickaxe", 0x007DE290, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragon Tooth", 0x007E09A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Smough's Great Hammer", 0x007E30B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Blacksmith Hammer", 0x007E57C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Morne's Great Hammer", 0x007E7ED0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Spiked Mace", 0x007EA5E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Spear", 0x00895440, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Spear", 0x00897B50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Partizan", 0x0089C970, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatlance", 0x008A8CC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Long Spear", 0x008AB3D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gargoyle Flame Spear", 0x008B01F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Rotten Ghru Spear", 0x008B2900, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Tailbone Spear", 0x008B5010, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Soldering Iron", 0x008B7720, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Arstor's Spear", 0x008BEC50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Saint Bident", 0x008C1360, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Yorshka's Spear", 0x008C3A70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pike", 0x008C6180, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Heavy Four-pronged Plow", 0x008ADAE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragonslayer Spear", 0x008CAFA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Scythe", 0x00989680, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lucerne", 0x0098BD90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Glaive", 0x0098E4A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Halberd", 0x00990BB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Greataxe", 0x009959D0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pontiff Knight Great Scythe", 0x0099A7F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Corvian Scythe", 0x0099CF00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Knight Halberd", 0x0099F610, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gundyr's Halberd", 0x009A1D20, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Red Hilted Halberd", 0x009AB960, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Glaive", 0x009AE070, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Immolation Tinder", 0x009B0780, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Claw", 0x00A7D8C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Caestus", 0x00A7FFD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Manikin Claws", 0x00A826E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Demon's Fist", 0x00A84DF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Dark Hand", 0x00A87500, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Whip", 0x00B71B00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Witch's Locks", 0x00B7B740, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Notched Whip", 0x00B7DE50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Spotted Whip", 0x00B80560, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Talisman", 0x00C72090, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sorcerer's Staff", 0x00C747A0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Storyteller's Staff", 0x00C76EB0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Mendicant's Staff", 0x00C795C0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Man-grub's Staff", 0x00C7E3E0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Archdeacon's Great Staff", 0x00C80AF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Golden Ritual Spear", 0x00C83200, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Yorshka's Chime", 0x00C88020, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sage's Crystal Staff", 0x00C8CE40, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Heretic's Staff", 0x00C8F550, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Court Sorcerer's Staff", 0x00C91C60, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Witchtree Branch", 0x00C94370, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Izalith Staff", 0x00C96A80, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cleric's Sacred Chime", 0x00C99190, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Priest's Chime", 0x00C9B8A0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Saint-tree Bellvine", 0x00C9DFB0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Caitha's Chime", 0x00CA06C0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crystal Chime", 0x00CA2DD0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Sunlight Talisman", 0x00CA54E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Canvas Talisman", 0x00CA7BF0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sunless Talisman", 0x00CAA300, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Saint's Talisman", 0x00CACA10, DS3ItemCategory.WEAPON_UPGRADE_10), - ("White Hair Talisman", 0x00CAF120, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pyromancy Flame", 0x00CC77C0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonslayer Greatbow", 0x00CF8500, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Short Bow", 0x00D5C690, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Composite Bow", 0x00D5EDA0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Light Crossbow", 0x00D63BC0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Arbalest", 0x00D662D0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Longbow", 0x00D689E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonrider Bow", 0x00D6B0F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Avelyn", 0x00D6FF10, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Knight's Crossbow", 0x00D72620, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Heavy Crossbow", 0x00D74D30, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Darkmoon Longbow", 0x00D79B50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Onislayer Greatbow", 0x00D7C260, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Black Bow of Pharis", 0x00D7E970, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sniper Crossbow", 0x00D83790, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sellsword Twinblades", 0x00F42400, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Warden Twinblades", 0x00F47220, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Knight Twinaxes", 0x00F49930, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dancer's Enchanted Swords", 0x00F4C040, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Machete", 0x00F4E750, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Brigand Twindaggers", 0x00F50E60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gotthard Twinswords", 0x00F53570, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Onikiri and Ubadachi", 0x00F58390, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Drang Twinspears", 0x00F5AAA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Drang Hammers", 0x00F61FD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dagger", 0x000F4240, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Bandit's Knife", 0x000F6950, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Parrying Dagger", 0x000F9060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rotten Ghru Dagger", 0x000FDE80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Harpe", 0x00102CA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Scholar's Candlestick", 0x001053B0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Tailbone Short Sword", 0x00107AC0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Corvian Greatknife", 0x0010A1D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Handmaid's Dagger", 0x00111700, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Shortsword", 0x001E8480, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Longsword", 0x001EAB90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Broadsword", 0x001ED2A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Broken Straight Sword", 0x001EF9B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Sword", 0x001F6EE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Sunlight Straight Sword", 0x00203230, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Rotten Ghru Curved Sword", 0x00205940, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Irithyll Straight Sword", 0x0020A760, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cleric's Candlestick", 0x0020F580, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Morion Blade", 0x002143A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Astora Straight Sword", 0x002191C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Barbed Straight Sword", 0x0021B8D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Executioner's Greatsword", 0x0021DFE0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Anri's Straight Sword", 0x002206F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Estoc", 0x002DC6C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Mail Breaker", 0x002DEDD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rapier", 0x002E14E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ricard's Rapier", 0x002E3BF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crystal Sage's Rapier", 0x002E6300, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Irithyll Rapier", 0x002E8A10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Shotel", 0x003D3010, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Scimitar", 0x003D7E30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Falchion", 0x003DA540, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Carthus Curved Sword", 0x003DCC50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Carthus Curved Greatsword", 0x003DF360, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Pontiff Knight Curved Sword", 0x003E1A70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Storm Curved Sword", 0x003E4180, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Painting Guardian's Curved Sword", 0x003E6890, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crescent Moon Sword", 0x003E8FA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Carthus Shotel", 0x003EB6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Uchigatana", 0x004C4B40, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Washing Pole", 0x004C7250, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Chaos Blade", 0x004C9960, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Black Blade", 0x004CC070, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Bloodlust", 0x004CE780, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Darkdrift", 0x004D0E90, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Bastard Sword", 0x005B8D80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Claymore", 0x005BDBA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Zweihander", 0x005C29C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatsword", 0x005C50D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Astora Greatsword", 0x005C9EF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Murakumo", 0x005CC600, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Greatsword", 0x005D1420, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Flamberge", 0x005DB060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Exile Greatsword", 0x005DD770, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatsword of Judgment", 0x005E2590, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Profaned Greatsword", 0x005E4CA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cathedral Knight Greatsword", 0x005E73B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Farron Greatsword", 0x005E9AC0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Yhorm's Great Machete", 0x005F0FF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Dark Sword", 0x005F3700, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Sword", 0x005F5E10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lorian's Greatsword", 0x005F8520, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Twin Princes' Greatsword", 0x005FAC30, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lothric's Holy Sword", 0x005FD340, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Wolnir's Holy Sword", 0x005FFA50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Wolf Knight's Greatsword", 0x00602160, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Greatsword of Artorias", 0x0060216A, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Hollowslayer Greatsword", 0x00604870, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Moonlight Greatsword", 0x00606F80, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Drakeblood Greatsword", 0x00609690, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Firelink Greatsword", 0x0060BDA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Fume Ultra Greatsword", 0x0060E4B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Old Wolf Curved Sword", 0x00610BC0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Storm Ruler", 0x006132D0, DS3ItemCategory.WEAPON_UPGRADE_5, + classification = ItemClassification.progression), + DS3ItemData("Hand Axe", 0x006ACFC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Battle Axe", 0x006AF6D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Deep Battle Axe", 0x006AFA54, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Brigand Axe", 0x006B1DE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crescent Axe", 0x006B6C00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greataxe", 0x006B9310, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Butcher Knife", 0x006BE130, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonslayer's Axe", 0x006C0840, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Thrall Axe", 0x006C5660, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragonslayer Greataxe", 0x006C7D70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Demon's Greataxe", 0x006CA480, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Eleonora", 0x006CCB90, DS3ItemCategory.WEAPON_UPGRADE_5, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Man Serpent Hatchet", 0x006D19B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Club", 0x007A1200, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Mace", 0x007A3910, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Morning Star", 0x007A6020, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Reinforced Club", 0x007A8730, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Large Club", 0x007AFC60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Club", 0x007B4A80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Mace", 0x007BBFB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Wooden Hammer", 0x007C8300, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gargoyle Flame Hammer", 0x007CAA10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Vordt's Great Hammer", 0x007CD120, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Old King's Great Hammer", 0x007CF830, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Heysel Pick", 0x007D6D60, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Warpick", 0x007DBB80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Pickaxe", 0x007DE290, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragon Tooth", 0x007E09A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Smough's Great Hammer", 0x007E30B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Blacksmith Hammer", 0x007E57C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Morne's Great Hammer", 0x007E7ED0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Spiked Mace", 0x007EA5E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Spear", 0x00895440, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Spear", 0x00897B50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Partizan", 0x0089C970, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatlance", 0x008A8CC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Long Spear", 0x008AB3D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gargoyle Flame Spear", 0x008B01F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Rotten Ghru Spear", 0x008B2900, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Tailbone Spear", 0x008B5010, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Soldering Iron", 0x008B7720, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonslayer Swordspear", 0x008BC540, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Arstor's Spear", 0x008BEC50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Saint Bident", 0x008C1360, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Yorshka's Spear", 0x008C3A70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pike", 0x008C6180, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Heavy Four-pronged Plow", 0x008ADAE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragonslayer Spear", 0x008CAFA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Scythe", 0x00989680, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lucerne", 0x0098BD90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Glaive", 0x0098E4A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Halberd", 0x00990BB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Greataxe", 0x009959D0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pontiff Knight Great Scythe", 0x0099A7F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Corvian Scythe", 0x0099CF00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Knight Halberd", 0x0099F610, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gundyr's Halberd", 0x009A1D20, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Red Hilted Halberd", 0x009AB960, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Glaive", 0x009AE070, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Immolation Tinder", 0x009B0780, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Claw", 0x00A7D8C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Caestus", 0x00A7FFD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Manikin Claws", 0x00A826E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Demon's Fist", 0x00A84DF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Dark Hand", 0x00A87500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Whip", 0x00B71B00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Witch's Locks", 0x00B7B740, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Notched Whip", 0x00B7DE50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Spotted Whip", 0x00B80560, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Talisman", 0x00C72090, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sorcerer's Staff", 0x00C747A0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Storyteller's Staff", 0x00C76EB0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Mendicant's Staff", 0x00C795C0, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression, # Crow trade + inject = True), # This is just a random drop normally, but we need it in-logic + DS3ItemData("Man-grub's Staff", 0x00C7E3E0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Archdeacon's Great Staff", 0x00C80AF0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Golden Ritual Spear", 0x00C83200, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Yorshka's Chime", 0x00C88020, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sage's Crystal Staff", 0x00C8CE40, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Heretic's Staff", 0x00C8F550, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Court Sorcerer's Staff", 0x00C91C60, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Witchtree Branch", 0x00C94370, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Izalith Staff", 0x00C96A80, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cleric's Sacred Chime", 0x00C99190, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Priest's Chime", 0x00C9B8A0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Saint-tree Bellvine", 0x00C9DFB0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Caitha's Chime", 0x00CA06C0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crystal Chime", 0x00CA2DD0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Sunlight Talisman", 0x00CA54E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Canvas Talisman", 0x00CA7BF0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sunless Talisman", 0x00CAA300, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Saint's Talisman", 0x00CACA10, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("White Hair Talisman", 0x00CAF120, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pyromancy Flame", 0x00CC77C0, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression), + DS3ItemData("Dragonslayer Greatbow", 0x00CF8500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Short Bow", 0x00D5C690, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Composite Bow", 0x00D5EDA0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Light Crossbow", 0x00D63BC0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Arbalest", 0x00D662D0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Longbow", 0x00D689E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonrider Bow", 0x00D6B0F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Avelyn", 0x00D6FF10, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Knight's Crossbow", 0x00D72620, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Heavy Crossbow", 0x00D74D30, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Darkmoon Longbow", 0x00D79B50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Onislayer Greatbow", 0x00D7C260, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Black Bow of Pharis", 0x00D7E970, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sniper Crossbow", 0x00D83790, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sellsword Twinblades", 0x00F42400, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Warden Twinblades", 0x00F47220, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Knight Twinaxes", 0x00F49930, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dancer's Enchanted Swords", 0x00F4C040, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Machete", 0x00F4E750, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Brigand Twindaggers", 0x00F50E60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gotthard Twinswords", 0x00F53570, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Onikiri and Ubadachi", 0x00F58390, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Drang Twinspears", 0x00F5AAA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Drang Hammers", 0x00F61FD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), # Shields - ("Buckler", 0x01312D00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Small Leather Shield", 0x01315410, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Round Shield", 0x0131A230, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Large Leather Shield", 0x0131C940, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Hawkwood's Shield", 0x01323E70, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Iron Round Shield", 0x01326580, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Wooden Shield", 0x0132DAB0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Kite Shield", 0x013301C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Ghru Rotshield", 0x013328D0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Havel's Greatshield", 0x013376F0, DS3ItemCategory.SHIELD), - ("Target Shield", 0x01339E00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Elkhorn Round Shield", 0x0133C510, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Warrior's Round Shield", 0x0133EC20, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Caduceus Round Shield", 0x01341330, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Red and White Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Blessed Red and White Shield+1", 0x01343FB9, DS3ItemCategory.SHIELD), - ("Plank Shield", 0x01346150, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Leather Shield", 0x01348860, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Crimson Parma", 0x0134AF70, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Eastern Iron Shield", 0x0134D680, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Llewellyn Shield", 0x0134FD90, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Golden Falcon Shield", 0x01354BB0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Sacred Bloom Shield", 0x013572C0, DS3ItemCategory.SHIELD), - ("Ancient Dragon Greatshield", 0x013599D0, DS3ItemCategory.SHIELD), - ("Lothric Knight Shield", 0x01409650, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Knight Shield", 0x01410B80, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Pontiff Knight Shield", 0x014159A0, DS3ItemCategory.SHIELD), - ("Carthus Shield", 0x014180B0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Black Knight Shield", 0x0141F5E0, DS3ItemCategory.SHIELD), - ("Silver Knight Shield", 0x01424400, DS3ItemCategory.SHIELD), - ("Spiked Shield", 0x01426B10, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Pierce Shield", 0x01429220, DS3ItemCategory.SHIELD_INFUSIBLE), - ("East-West Shield", 0x0142B930, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Sunlight Shield", 0x0142E040, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Crest Shield", 0x01430750, DS3ItemCategory.SHIELD), - ("Dragon Crest Shield", 0x01432E60, DS3ItemCategory.SHIELD), - ("Spider Shield", 0x01435570, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Grass Crest Shield", 0x01437C80, DS3ItemCategory.SHIELD), - ("Sunset Shield", 0x0143A390, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Golden Wing Crest Shield", 0x0143CAA0, DS3ItemCategory.SHIELD), - ("Blue Wooden Shield", 0x0143F1B0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Silver Eagle Kite Shield", 0x014418C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Stone Parma", 0x01443FD0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Spirit Tree Crest Shield", 0x014466E0, DS3ItemCategory.SHIELD), - ("Porcine Shield", 0x01448DF0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Shield of Want", 0x0144B500, DS3ItemCategory.SHIELD), - ("Wargod Wooden Shield", 0x0144DC10, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Lothric Knight Greatshield", 0x014FD890, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Cathedral Knight Greatshield", 0x014FFFA0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Dragonslayer Greatshield", 0x01504DC0, DS3ItemCategory.SHIELD), - ("Moaning Shield", 0x015074D0, DS3ItemCategory.SHIELD), - ("Yhorm's Greatshield", 0x0150C2F0, DS3ItemCategory.SHIELD), - ("Black Iron Greatshield", 0x0150EA00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Wolf Knight's Greatshield", 0x01511110, DS3ItemCategory.SHIELD), - ("Twin Dragon Greatshield", 0x01513820, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Greatshield of Glory", 0x01515F30, DS3ItemCategory.SHIELD), - ("Curse Ward Greatshield", 0x01518640, DS3ItemCategory.SHIELD), - ("Bonewheel Shield", 0x0151AD50, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Stone Greatshield", 0x0151D460, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Buckler", 0x01312D00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Small Leather Shield", 0x01315410, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Round Shield", 0x0131A230, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Large Leather Shield", 0x0131C940, DS3ItemCategory.SHIELD_INFUSIBLE, + classification = ItemClassification.progression, # Crow trade + inject = True), # This is a shop/infinite drop item, but we need it in logic + DS3ItemData("Hawkwood's Shield", 0x01323E70, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Iron Round Shield", 0x01326580, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Wooden Shield", 0x0132DAB0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Kite Shield", 0x013301C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Ghru Rotshield", 0x013328D0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Havel's Greatshield", 0x013376F0, DS3ItemCategory.SHIELD), + DS3ItemData("Target Shield", 0x01339E00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Elkhorn Round Shield", 0x0133C510, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Warrior's Round Shield", 0x0133EC20, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Caduceus Round Shield", 0x01341330, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Red and White Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Blessed Red and White Shield+1", 0x01343FB9, DS3ItemCategory.SHIELD), + DS3ItemData("Plank Shield", 0x01346150, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Leather Shield", 0x01348860, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Crimson Parma", 0x0134AF70, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Eastern Iron Shield", 0x0134D680, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Llewellyn Shield", 0x0134FD90, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Golden Falcon Shield", 0x01354BB0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Sacred Bloom Shield", 0x013572C0, DS3ItemCategory.SHIELD), + DS3ItemData("Ancient Dragon Greatshield", 0x013599D0, DS3ItemCategory.SHIELD), + DS3ItemData("Lothric Knight Shield", 0x01409650, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Knight Shield", 0x01410B80, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Pontiff Knight Shield", 0x014159A0, DS3ItemCategory.SHIELD), + DS3ItemData("Carthus Shield", 0x014180B0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Black Knight Shield", 0x0141F5E0, DS3ItemCategory.SHIELD), + DS3ItemData("Silver Knight Shield", 0x01424400, DS3ItemCategory.SHIELD), + DS3ItemData("Spiked Shield", 0x01426B10, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Pierce Shield", 0x01429220, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("East-West Shield", 0x0142B930, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Sunlight Shield", 0x0142E040, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Crest Shield", 0x01430750, DS3ItemCategory.SHIELD), + DS3ItemData("Dragon Crest Shield", 0x01432E60, DS3ItemCategory.SHIELD), + DS3ItemData("Spider Shield", 0x01435570, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Grass Crest Shield", 0x01437C80, DS3ItemCategory.SHIELD, + classification = ItemClassification.useful), + DS3ItemData("Sunset Shield", 0x0143A390, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Golden Wing Crest Shield", 0x0143CAA0, DS3ItemCategory.SHIELD), + DS3ItemData("Blue Wooden Shield", 0x0143F1B0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Silver Eagle Kite Shield", 0x014418C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Stone Parma", 0x01443FD0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Spirit Tree Crest Shield", 0x014466E0, DS3ItemCategory.SHIELD), + DS3ItemData("Porcine Shield", 0x01448DF0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Shield of Want", 0x0144B500, DS3ItemCategory.SHIELD), + DS3ItemData("Wargod Wooden Shield", 0x0144DC10, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Lothric Knight Greatshield", 0x014FD890, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Cathedral Knight Greatshield", 0x014FFFA0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Dragonslayer Greatshield", 0x01504DC0, DS3ItemCategory.SHIELD), + DS3ItemData("Moaning Shield", 0x015074D0, DS3ItemCategory.SHIELD, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Yhorm's Greatshield", 0x0150C2F0, DS3ItemCategory.SHIELD), + DS3ItemData("Black Iron Greatshield", 0x0150EA00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Wolf Knight's Greatshield", 0x01511110, DS3ItemCategory.SHIELD, + inject = True), # Covenant reward + DS3ItemData("Twin Dragon Greatshield", 0x01513820, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Greatshield of Glory", 0x01515F30, DS3ItemCategory.SHIELD), + DS3ItemData("Curse Ward Greatshield", 0x01518640, DS3ItemCategory.SHIELD), + DS3ItemData("Bonewheel Shield", 0x0151AD50, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Stone Greatshield", 0x0151D460, DS3ItemCategory.SHIELD_INFUSIBLE), # Armor - ("Fallen Knight Helm", 0x1121EAC0, DS3ItemCategory.ARMOR), - ("Fallen Knight Armor", 0x1121EEA8, DS3ItemCategory.ARMOR), - ("Fallen Knight Gauntlets", 0x1121F290, DS3ItemCategory.ARMOR), - ("Fallen Knight Trousers", 0x1121F678, DS3ItemCategory.ARMOR), - ("Knight Helm", 0x11298BE0, DS3ItemCategory.ARMOR), - ("Knight Armor", 0x11298FC8, DS3ItemCategory.ARMOR), - ("Knight Gauntlets", 0x112993B0, DS3ItemCategory.ARMOR), - ("Knight Leggings", 0x11299798, DS3ItemCategory.ARMOR), - ("Firelink Helm", 0x11406F40, DS3ItemCategory.ARMOR), - ("Firelink Armor", 0x11407328, DS3ItemCategory.ARMOR), - ("Firelink Gauntlets", 0x11407710, DS3ItemCategory.ARMOR), - ("Firelink Leggings", 0x11407AF8, DS3ItemCategory.ARMOR), - ("Sellsword Helm", 0x11481060, DS3ItemCategory.ARMOR), - ("Sellsword Armor", 0x11481448, DS3ItemCategory.ARMOR), - ("Sellsword Gauntlet", 0x11481830, DS3ItemCategory.ARMOR), - ("Sellsword Trousers", 0x11481C18, DS3ItemCategory.ARMOR), - ("Herald Helm", 0x114FB180, DS3ItemCategory.ARMOR), - ("Herald Armor", 0x114FB568, DS3ItemCategory.ARMOR), - ("Herald Gloves", 0x114FB950, DS3ItemCategory.ARMOR), - ("Herald Trousers", 0x114FBD38, DS3ItemCategory.ARMOR), - ("Sunless Veil", 0x115752A0, DS3ItemCategory.ARMOR), - ("Sunless Armor", 0x11575688, DS3ItemCategory.ARMOR), - ("Sunless Gauntlets", 0x11575A70, DS3ItemCategory.ARMOR), - ("Sunless Leggings", 0x11575E58, DS3ItemCategory.ARMOR), - ("Black Hand Hat", 0x115EF3C0, DS3ItemCategory.ARMOR), - ("Black Hand Armor", 0x115EF7A8, DS3ItemCategory.ARMOR), - ("Assassin Gloves", 0x115EFB90, DS3ItemCategory.ARMOR), - ("Assassin Trousers", 0x115EFF78, DS3ItemCategory.ARMOR), - ("Assassin Hood", 0x11607A60, DS3ItemCategory.ARMOR), - ("Assassin Armor", 0x11607E48, DS3ItemCategory.ARMOR), - ("Xanthous Crown", 0x116694E0, DS3ItemCategory.ARMOR), - ("Xanthous Overcoat", 0x116698C8, DS3ItemCategory.ARMOR), - ("Xanthous Gloves", 0x11669CB0, DS3ItemCategory.ARMOR), - ("Xanthous Trousers", 0x1166A098, DS3ItemCategory.ARMOR), - ("Northern Helm", 0x116E3600, DS3ItemCategory.ARMOR), - ("Northern Armor", 0x116E39E8, DS3ItemCategory.ARMOR), - ("Northern Gloves", 0x116E3DD0, DS3ItemCategory.ARMOR), - ("Northern Trousers", 0x116E41B8, DS3ItemCategory.ARMOR), - ("Morne's Helm", 0x1175D720, DS3ItemCategory.ARMOR), - ("Morne's Armor", 0x1175DB08, DS3ItemCategory.ARMOR), - ("Morne's Gauntlets", 0x1175DEF0, DS3ItemCategory.ARMOR), - ("Morne's Leggings", 0x1175E2D8, DS3ItemCategory.ARMOR), - ("Silver Mask", 0x117D7840, DS3ItemCategory.ARMOR), - ("Leonhard's Garb", 0x117D7C28, DS3ItemCategory.ARMOR), - ("Leonhard's Gauntlets", 0x117D8010, DS3ItemCategory.ARMOR), - ("Leonhard's Trousers", 0x117D83F8, DS3ItemCategory.ARMOR), - ("Sneering Mask", 0x11851960, DS3ItemCategory.ARMOR), - ("Pale Shade Robe", 0x11851D48, DS3ItemCategory.ARMOR), - ("Pale Shade Gloves", 0x11852130, DS3ItemCategory.ARMOR), - ("Pale Shade Trousers", 0x11852518, DS3ItemCategory.ARMOR), - ("Sunset Helm", 0x118CBA80, DS3ItemCategory.ARMOR), - ("Sunset Armor", 0x118CBE68, DS3ItemCategory.ARMOR), - ("Sunset Gauntlets", 0x118CC250, DS3ItemCategory.ARMOR), - ("Sunset Leggings", 0x118CC638, DS3ItemCategory.ARMOR), - ("Old Sage's Blindfold", 0x11945BA0, DS3ItemCategory.ARMOR), - ("Cornyx's Garb", 0x11945F88, DS3ItemCategory.ARMOR), - ("Cornyx's Wrap", 0x11946370, DS3ItemCategory.ARMOR), - ("Cornyx's Skirt", 0x11946758, DS3ItemCategory.ARMOR), - ("Executioner Helm", 0x119BFCC0, DS3ItemCategory.ARMOR), - ("Executioner Armor", 0x119C00A8, DS3ItemCategory.ARMOR), - ("Executioner Gauntlets", 0x119C0490, DS3ItemCategory.ARMOR), - ("Executioner Leggings", 0x119C0878, DS3ItemCategory.ARMOR), - ("Billed Mask", 0x11A39DE0, DS3ItemCategory.ARMOR), - ("Black Dress", 0x11A3A1C8, DS3ItemCategory.ARMOR), - ("Black Gauntlets", 0x11A3A5B0, DS3ItemCategory.ARMOR), - ("Black Leggings", 0x11A3A998, DS3ItemCategory.ARMOR), - ("Pyromancer Crown", 0x11AB3F00, DS3ItemCategory.ARMOR), - ("Pyromancer Garb", 0x11AB42E8, DS3ItemCategory.ARMOR), - ("Pyromancer Wrap", 0x11AB46D0, DS3ItemCategory.ARMOR), - ("Pyromancer Trousers", 0x11AB4AB8, DS3ItemCategory.ARMOR), - ("Court Sorcerer Hood", 0x11BA8140, DS3ItemCategory.ARMOR), - ("Court Sorcerer Robe", 0x11BA8528, DS3ItemCategory.ARMOR), - ("Court Sorcerer Gloves", 0x11BA8910, DS3ItemCategory.ARMOR), - ("Court Sorcerer Trousers", 0x11BA8CF8, DS3ItemCategory.ARMOR), - ("Sorcerer Hood", 0x11C9C380, DS3ItemCategory.ARMOR), - ("Sorcerer Robe", 0x11C9C768, DS3ItemCategory.ARMOR), - ("Sorcerer Gloves", 0x11C9CB50, DS3ItemCategory.ARMOR), - ("Sorcerer Trousers", 0x11C9CF38, DS3ItemCategory.ARMOR), - ("Clandestine Coat", 0x11CB4E08, DS3ItemCategory.ARMOR), - ("Cleric Hat", 0x11D905C0, DS3ItemCategory.ARMOR), - ("Cleric Blue Robe", 0x11D909A8, DS3ItemCategory.ARMOR), - ("Cleric Gloves", 0x11D90D90, DS3ItemCategory.ARMOR), - ("Cleric Trousers", 0x11D91178, DS3ItemCategory.ARMOR), - ("Steel Soldier Helm", 0x12625A00, DS3ItemCategory.ARMOR), - ("Deserter Armor", 0x12625DE8, DS3ItemCategory.ARMOR), - ("Deserter Trousers", 0x126265B8, DS3ItemCategory.ARMOR), - ("Thief Mask", 0x12656740, DS3ItemCategory.ARMOR), - ("Sage's Big Hat", 0x129020C0, DS3ItemCategory.ARMOR), - ("Aristocrat's Mask", 0x129F6300, DS3ItemCategory.ARMOR), - ("Jailer Robe", 0x129F66E8, DS3ItemCategory.ARMOR), - ("Jailer Gloves", 0x129F6AD0, DS3ItemCategory.ARMOR), - ("Jailer Trousers", 0x129F6EB8, DS3ItemCategory.ARMOR), - ("Grave Warden Hood", 0x12BDE780, DS3ItemCategory.ARMOR), - ("Grave Warden Robe", 0x12BDEB68, DS3ItemCategory.ARMOR), - ("Grave Warden Wrap", 0x12BDEF50, DS3ItemCategory.ARMOR), - ("Grave Warden Skirt", 0x12BDF338, DS3ItemCategory.ARMOR), - ("Worker Hat", 0x12CD29C0, DS3ItemCategory.ARMOR), - ("Worker Garb", 0x12CD2DA8, DS3ItemCategory.ARMOR), - ("Worker Gloves", 0x12CD3190, DS3ItemCategory.ARMOR), - ("Worker Trousers", 0x12CD3578, DS3ItemCategory.ARMOR), - ("Thrall Hood", 0x12D4CAE0, DS3ItemCategory.ARMOR), - ("Evangelist Hat", 0x12DC6C00, DS3ItemCategory.ARMOR), - ("Evangelist Robe", 0x12DC6FE8, DS3ItemCategory.ARMOR), - ("Evangelist Gloves", 0x12DC73D0, DS3ItemCategory.ARMOR), - ("Evangelist Trousers", 0x12DC77B8, DS3ItemCategory.ARMOR), - ("Scholar's Robe", 0x12E41108, DS3ItemCategory.ARMOR), - ("Winged Knight Helm", 0x12EBAE40, DS3ItemCategory.ARMOR), - ("Winged Knight Armor", 0x12EBB228, DS3ItemCategory.ARMOR), - ("Winged Knight Gauntlets", 0x12EBB610, DS3ItemCategory.ARMOR), - ("Winged Knight Leggings", 0x12EBB9F8, DS3ItemCategory.ARMOR), - ("Cathedral Knight Helm", 0x130291A0, DS3ItemCategory.ARMOR), - ("Cathedral Knight Armor", 0x13029588, DS3ItemCategory.ARMOR), - ("Cathedral Knight Gauntlets", 0x13029970, DS3ItemCategory.ARMOR), - ("Cathedral Knight Leggings", 0x13029D58, DS3ItemCategory.ARMOR), - ("Lothric Knight Helm", 0x13197500, DS3ItemCategory.ARMOR), - ("Lothric Knight Armor", 0x131978E8, DS3ItemCategory.ARMOR), - ("Lothric Knight Gauntlets", 0x13197CD0, DS3ItemCategory.ARMOR), - ("Lothric Knight Leggings", 0x131980B8, DS3ItemCategory.ARMOR), - ("Outrider Knight Helm", 0x1328B740, DS3ItemCategory.ARMOR), - ("Outrider Knight Armor", 0x1328BB28, DS3ItemCategory.ARMOR), - ("Outrider Knight Gauntlets", 0x1328BF10, DS3ItemCategory.ARMOR), - ("Outrider Knight Leggings", 0x1328C2F8, DS3ItemCategory.ARMOR), - ("Black Knight Helm", 0x1337F980, DS3ItemCategory.ARMOR), - ("Black Knight Armor", 0x1337FD68, DS3ItemCategory.ARMOR), - ("Black Knight Gauntlets", 0x13380150, DS3ItemCategory.ARMOR), - ("Black Knight Leggings", 0x13380538, DS3ItemCategory.ARMOR), - ("Dark Mask", 0x133F9AA0, DS3ItemCategory.ARMOR), - ("Dark Armor", 0x133F9E88, DS3ItemCategory.ARMOR), - ("Dark Gauntlets", 0x133FA270, DS3ItemCategory.ARMOR), - ("Dark Leggings", 0x133FA658, DS3ItemCategory.ARMOR), - ("Exile Mask", 0x13473BC0, DS3ItemCategory.ARMOR), - ("Exile Armor", 0x13473FA8, DS3ItemCategory.ARMOR), - ("Exile Gauntlets", 0x13474390, DS3ItemCategory.ARMOR), - ("Exile Leggings", 0x13474778, DS3ItemCategory.ARMOR), - ("Pontiff Knight Crown", 0x13567E00, DS3ItemCategory.ARMOR), - ("Pontiff Knight Armor", 0x135681E8, DS3ItemCategory.ARMOR), - ("Pontiff Knight Gauntlets", 0x135685D0, DS3ItemCategory.ARMOR), - ("Pontiff Knight Leggings", 0x135689B8, DS3ItemCategory.ARMOR), - ("Golden Crown", 0x1365C040, DS3ItemCategory.ARMOR), - ("Dragonscale Armor", 0x1365C428, DS3ItemCategory.ARMOR), - ("Golden Bracelets", 0x1365C810, DS3ItemCategory.ARMOR), - ("Dragonscale Waistcloth", 0x1365CBF8, DS3ItemCategory.ARMOR), - ("Wolnir's Crown", 0x136D6160, DS3ItemCategory.ARMOR), - ("Undead Legion Helm", 0x13750280, DS3ItemCategory.ARMOR), - ("Undead Legion Armor", 0x13750668, DS3ItemCategory.ARMOR), - ("Undead Legion Gauntlets", 0x13750A50, DS3ItemCategory.ARMOR), - ("Undead Legion Leggings", 0x13750E38, DS3ItemCategory.ARMOR), - ("Fire Witch Helm", 0x13938700, DS3ItemCategory.ARMOR), - ("Fire Witch Armor", 0x13938AE8, DS3ItemCategory.ARMOR), - ("Fire Witch Gauntlets", 0x13938ED0, DS3ItemCategory.ARMOR), - ("Fire Witch Leggings", 0x139392B8, DS3ItemCategory.ARMOR), - ("Lorian's Helm", 0x13A2C940, DS3ItemCategory.ARMOR), - ("Lorian's Armor", 0x13A2CD28, DS3ItemCategory.ARMOR), - ("Lorian's Gauntlets", 0x13A2D110, DS3ItemCategory.ARMOR), - ("Lorian's Leggings", 0x13A2D4F8, DS3ItemCategory.ARMOR), - ("Hood of Prayer", 0x13AA6A60, DS3ItemCategory.ARMOR), - ("Robe of Prayer", 0x13AA6E48, DS3ItemCategory.ARMOR), - ("Skirt of Prayer", 0x13AA7618, DS3ItemCategory.ARMOR), - ("Dancer's Crown", 0x13C14DC0, DS3ItemCategory.ARMOR), - ("Dancer's Armor", 0x13C151A8, DS3ItemCategory.ARMOR), - ("Dancer's Gauntlets", 0x13C15590, DS3ItemCategory.ARMOR), - ("Dancer's Leggings", 0x13C15978, DS3ItemCategory.ARMOR), - ("Gundyr's Helm", 0x13D09000, DS3ItemCategory.ARMOR), - ("Gundyr's Armor", 0x13D093E8, DS3ItemCategory.ARMOR), - ("Gundyr's Gauntlets", 0x13D097D0, DS3ItemCategory.ARMOR), - ("Gundyr's Leggings", 0x13D09BB8, DS3ItemCategory.ARMOR), - ("Archdeacon White Crown", 0x13EF1480, DS3ItemCategory.ARMOR), - ("Archdeacon Holy Garb", 0x13EF1868, DS3ItemCategory.ARMOR), - ("Archdeacon Skirt", 0x13EF2038, DS3ItemCategory.ARMOR), - ("Deacon Robe", 0x13F6B988, DS3ItemCategory.ARMOR), - ("Deacon Skirt", 0x13F6C158, DS3ItemCategory.ARMOR), - ("Fire Keeper Robe", 0x140D9CE8, DS3ItemCategory.ARMOR), - ("Fire Keeper Gloves", 0x140DA0D0, DS3ItemCategory.ARMOR), - ("Fire Keeper Skirt", 0x140DA4B8, DS3ItemCategory.ARMOR), - ("Chain Helm", 0x142C1D80, DS3ItemCategory.ARMOR), - ("Chain Armor", 0x142C2168, DS3ItemCategory.ARMOR), - ("Leather Gauntlets", 0x142C2550, DS3ItemCategory.ARMOR), - ("Chain Leggings", 0x142C2938, DS3ItemCategory.ARMOR), - ("Nameless Knight Helm", 0x143B5FC0, DS3ItemCategory.ARMOR), - ("Nameless Knight Armor", 0x143B63A8, DS3ItemCategory.ARMOR), - ("Nameless Knight Gauntlets", 0x143B6790, DS3ItemCategory.ARMOR), - ("Nameless Knight Leggings", 0x143B6B78, DS3ItemCategory.ARMOR), - ("Elite Knight Helm", 0x144AA200, DS3ItemCategory.ARMOR), - ("Elite Knight Armor", 0x144AA5E8, DS3ItemCategory.ARMOR), - ("Elite Knight Gauntlets", 0x144AA9D0, DS3ItemCategory.ARMOR), - ("Elite Knight Leggings", 0x144AADB8, DS3ItemCategory.ARMOR), - ("Faraam Helm", 0x1459E440, DS3ItemCategory.ARMOR), - ("Faraam Armor", 0x1459E828, DS3ItemCategory.ARMOR), - ("Faraam Gauntlets", 0x1459EC10, DS3ItemCategory.ARMOR), - ("Faraam Boots", 0x1459EFF8, DS3ItemCategory.ARMOR), - ("Catarina Helm", 0x14692680, DS3ItemCategory.ARMOR), - ("Catarina Armor", 0x14692A68, DS3ItemCategory.ARMOR), - ("Catarina Gauntlets", 0x14692E50, DS3ItemCategory.ARMOR), - ("Catarina Leggings", 0x14693238, DS3ItemCategory.ARMOR), - ("Standard Helm", 0x1470C7A0, DS3ItemCategory.ARMOR), - ("Hard Leather Armor", 0x1470CB88, DS3ItemCategory.ARMOR), - ("Hard Leather Gauntlets", 0x1470CF70, DS3ItemCategory.ARMOR), - ("Hard Leather Boots", 0x1470D358, DS3ItemCategory.ARMOR), - ("Havel's Helm", 0x147868C0, DS3ItemCategory.ARMOR), - ("Havel's Armor", 0x14786CA8, DS3ItemCategory.ARMOR), - ("Havel's Gauntlets", 0x14787090, DS3ItemCategory.ARMOR), - ("Havel's Leggings", 0x14787478, DS3ItemCategory.ARMOR), - ("Brigand Hood", 0x148009E0, DS3ItemCategory.ARMOR), - ("Brigand Armor", 0x14800DC8, DS3ItemCategory.ARMOR), - ("Brigand Gauntlets", 0x148011B0, DS3ItemCategory.ARMOR), - ("Brigand Trousers", 0x14801598, DS3ItemCategory.ARMOR), - ("Pharis's Hat", 0x1487AB00, DS3ItemCategory.ARMOR), - ("Leather Armor", 0x1487AEE8, DS3ItemCategory.ARMOR), - ("Leather Gloves", 0x1487B2D0, DS3ItemCategory.ARMOR), - ("Leather Boots", 0x1487B6B8, DS3ItemCategory.ARMOR), - ("Ragged Mask", 0x148F4C20, DS3ItemCategory.ARMOR), - ("Master's Attire", 0x148F5008, DS3ItemCategory.ARMOR), - ("Master's Gloves", 0x148F53F0, DS3ItemCategory.ARMOR), - ("Loincloth", 0x148F57D8, DS3ItemCategory.ARMOR), - ("Old Sorcerer Hat", 0x1496ED40, DS3ItemCategory.ARMOR), - ("Old Sorcerer Coat", 0x1496F128, DS3ItemCategory.ARMOR), - ("Old Sorcerer Gauntlets", 0x1496F510, DS3ItemCategory.ARMOR), - ("Old Sorcerer Boots", 0x1496F8F8, DS3ItemCategory.ARMOR), - ("Conjurator Hood", 0x149E8E60, DS3ItemCategory.ARMOR), - ("Conjurator Robe", 0x149E9248, DS3ItemCategory.ARMOR), - ("Conjurator Manchettes", 0x149E9630, DS3ItemCategory.ARMOR), - ("Conjurator Boots", 0x149E9A18, DS3ItemCategory.ARMOR), - ("Black Leather Armor", 0x14A63368, DS3ItemCategory.ARMOR), - ("Black Leather Gloves", 0x14A63750, DS3ItemCategory.ARMOR), - ("Black Leather Boots", 0x14A63B38, DS3ItemCategory.ARMOR), - ("Symbol of Avarice", 0x14ADD0A0, DS3ItemCategory.ARMOR), - ("Creighton's Steel Mask", 0x14B571C0, DS3ItemCategory.ARMOR), - ("Mirrah Chain Mail", 0x14B575A8, DS3ItemCategory.ARMOR), - ("Mirrah Chain Gloves", 0x14B57990, DS3ItemCategory.ARMOR), - ("Mirrah Chain Leggings", 0x14B57D78, DS3ItemCategory.ARMOR), - ("Maiden Hood", 0x14BD12E0, DS3ItemCategory.ARMOR), - ("Maiden Robe", 0x14BD16C8, DS3ItemCategory.ARMOR), - ("Maiden Gloves", 0x14BD1AB0, DS3ItemCategory.ARMOR), - ("Maiden Skirt", 0x14BD1E98, DS3ItemCategory.ARMOR), - ("Alva Helm", 0x14C4B400, DS3ItemCategory.ARMOR), - ("Alva Armor", 0x14C4B7E8, DS3ItemCategory.ARMOR), - ("Alva Gauntlets", 0x14C4BBD0, DS3ItemCategory.ARMOR), - ("Alva Leggings", 0x14C4BFB8, DS3ItemCategory.ARMOR), - ("Shadow Mask", 0x14D3F640, DS3ItemCategory.ARMOR), - ("Shadow Garb", 0x14D3FA28, DS3ItemCategory.ARMOR), - ("Shadow Gauntlets", 0x14D3FE10, DS3ItemCategory.ARMOR), - ("Shadow Leggings", 0x14D401F8, DS3ItemCategory.ARMOR), - ("Eastern Helm", 0x14E33880, DS3ItemCategory.ARMOR), - ("Eastern Armor", 0x14E33C68, DS3ItemCategory.ARMOR), - ("Eastern Gauntlets", 0x14E34050, DS3ItemCategory.ARMOR), - ("Eastern Leggings", 0x14E34438, DS3ItemCategory.ARMOR), - ("Helm of Favor", 0x14F27AC0, DS3ItemCategory.ARMOR), - ("Embraced Armor of Favor", 0x14F27EA8, DS3ItemCategory.ARMOR), - ("Gauntlets of Favor", 0x14F28290, DS3ItemCategory.ARMOR), - ("Leggings of Favor", 0x14F28678, DS3ItemCategory.ARMOR), - ("Brass Helm", 0x1501BD00, DS3ItemCategory.ARMOR), - ("Brass Armor", 0x1501C0E8, DS3ItemCategory.ARMOR), - ("Brass Gauntlets", 0x1501C4D0, DS3ItemCategory.ARMOR), - ("Brass Leggings", 0x1501C8B8, DS3ItemCategory.ARMOR), - ("Silver Knight Helm", 0x1510FF40, DS3ItemCategory.ARMOR), - ("Silver Knight Armor", 0x15110328, DS3ItemCategory.ARMOR), - ("Silver Knight Gauntlets", 0x15110710, DS3ItemCategory.ARMOR), - ("Silver Knight Leggings", 0x15110AF8, DS3ItemCategory.ARMOR), - ("Lucatiel's Mask", 0x15204180, DS3ItemCategory.ARMOR), - ("Mirrah Vest", 0x15204568, DS3ItemCategory.ARMOR), - ("Mirrah Gloves", 0x15204950, DS3ItemCategory.ARMOR), - ("Mirrah Trousers", 0x15204D38, DS3ItemCategory.ARMOR), - ("Iron Helm", 0x152F83C0, DS3ItemCategory.ARMOR), - ("Armor of the Sun", 0x152F87A8, DS3ItemCategory.ARMOR), - ("Iron Bracelets", 0x152F8B90, DS3ItemCategory.ARMOR), - ("Iron Leggings", 0x152F8F78, DS3ItemCategory.ARMOR), - ("Drakeblood Helm", 0x153EC600, DS3ItemCategory.ARMOR), - ("Drakeblood Armor", 0x153EC9E8, DS3ItemCategory.ARMOR), - ("Drakeblood Gauntlets", 0x153ECDD0, DS3ItemCategory.ARMOR), - ("Drakeblood Leggings", 0x153ED1B8, DS3ItemCategory.ARMOR), - ("Drang Armor", 0x154E0C28, DS3ItemCategory.ARMOR), - ("Drang Gauntlets", 0x154E1010, DS3ItemCategory.ARMOR), - ("Drang Shoes", 0x154E13F8, DS3ItemCategory.ARMOR), - ("Black Iron Helm", 0x155D4A80, DS3ItemCategory.ARMOR), - ("Black Iron Armor", 0x155D4E68, DS3ItemCategory.ARMOR), - ("Black Iron Gauntlets", 0x155D5250, DS3ItemCategory.ARMOR), - ("Black Iron Leggings", 0x155D5638, DS3ItemCategory.ARMOR), - ("Painting Guardian Hood", 0x156C8CC0, DS3ItemCategory.ARMOR), - ("Painting Guardian Gown", 0x156C90A8, DS3ItemCategory.ARMOR), - ("Painting Guardian Gloves", 0x156C9490, DS3ItemCategory.ARMOR), - ("Painting Guardian Waistcloth", 0x156C9878, DS3ItemCategory.ARMOR), - ("Wolf Knight Helm", 0x157BCF00, DS3ItemCategory.ARMOR), - ("Wolf Knight Armor", 0x157BD2E8, DS3ItemCategory.ARMOR), - ("Wolf Knight Gauntlets", 0x157BD6D0, DS3ItemCategory.ARMOR), - ("Wolf Knight Leggings", 0x157BDAB8, DS3ItemCategory.ARMOR), - ("Dragonslayer Helm", 0x158B1140, DS3ItemCategory.ARMOR), - ("Dragonslayer Armor", 0x158B1528, DS3ItemCategory.ARMOR), - ("Dragonslayer Gauntlets", 0x158B1910, DS3ItemCategory.ARMOR), - ("Dragonslayer Leggings", 0x158B1CF8, DS3ItemCategory.ARMOR), - ("Smough's Helm", 0x159A5380, DS3ItemCategory.ARMOR), - ("Smough's Armor", 0x159A5768, DS3ItemCategory.ARMOR), - ("Smough's Gauntlets", 0x159A5B50, DS3ItemCategory.ARMOR), - ("Smough's Leggings", 0x159A5F38, DS3ItemCategory.ARMOR), - ("Helm of Thorns", 0x15B8D800, DS3ItemCategory.ARMOR), - ("Armor of Thorns", 0x15B8DBE8, DS3ItemCategory.ARMOR), - ("Gauntlets of Thorns", 0x15B8DFD0, DS3ItemCategory.ARMOR), - ("Leggings of Thorns", 0x15B8E3B8, DS3ItemCategory.ARMOR), - ("Crown of Dusk", 0x15D75C80, DS3ItemCategory.ARMOR), - ("Antiquated Dress", 0x15D76068, DS3ItemCategory.ARMOR), - ("Antiquated Gloves", 0x15D76450, DS3ItemCategory.ARMOR), - ("Antiquated Skirt", 0x15D76838, DS3ItemCategory.ARMOR), - ("Karla's Pointed Hat", 0x15E69EC0, DS3ItemCategory.ARMOR), - ("Karla's Coat", 0x15E6A2A8, DS3ItemCategory.ARMOR), - ("Karla's Gloves", 0x15E6A690, DS3ItemCategory.ARMOR), - ("Karla's Trousers", 0x15E6AA78, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Helm", 0x1121EAC0, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Armor", 0x1121EEA8, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Gauntlets", 0x1121F290, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Trousers", 0x1121F678, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Helm", 0x11298BE0, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Armor", 0x11298FC8, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Gauntlets", 0x112993B0, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Leggings", 0x11299798, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Helm", 0x11406F40, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Armor", 0x11407328, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Gauntlets", 0x11407710, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Leggings", 0x11407AF8, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Helm", 0x11481060, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Armor", 0x11481448, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Gauntlet", 0x11481830, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Trousers", 0x11481C18, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Helm", 0x114FB180, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Armor", 0x114FB568, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Gloves", 0x114FB950, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Trousers", 0x114FBD38, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Veil", 0x115752A0, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Armor", 0x11575688, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Gauntlets", 0x11575A70, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Leggings", 0x11575E58, DS3ItemCategory.ARMOR), + DS3ItemData("Black Hand Hat", 0x115EF3C0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Hand Armor", 0x115EF7A8, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Gloves", 0x115EFB90, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Trousers", 0x115EFF78, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Hood", 0x11607A60, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Armor", 0x11607E48, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Crown", 0x116694E0, DS3ItemCategory.ARMOR, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Xanthous Overcoat", 0x116698C8, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Gloves", 0x11669CB0, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Trousers", 0x1166A098, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Helm", 0x116E3600, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Armor", 0x116E39E8, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Gloves", 0x116E3DD0, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Trousers", 0x116E41B8, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Helm", 0x1175D720, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Armor", 0x1175DB08, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Gauntlets", 0x1175DEF0, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Leggings", 0x1175E2D8, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Mask", 0x117D7840, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Garb", 0x117D7C28, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Gauntlets", 0x117D8010, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Trousers", 0x117D83F8, DS3ItemCategory.ARMOR), + DS3ItemData("Sneering Mask", 0x11851960, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Robe", 0x11851D48, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Gloves", 0x11852130, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Trousers", 0x11852518, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Helm", 0x118CBA80, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Armor", 0x118CBE68, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Gauntlets", 0x118CC250, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Leggings", 0x118CC638, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sage's Blindfold", 0x11945BA0, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Garb", 0x11945F88, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Wrap", 0x11946370, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Skirt", 0x11946758, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Helm", 0x119BFCC0, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Armor", 0x119C00A8, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Gauntlets", 0x119C0490, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Leggings", 0x119C0878, DS3ItemCategory.ARMOR), + DS3ItemData("Billed Mask", 0x11A39DE0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Dress", 0x11A3A1C8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Gauntlets", 0x11A3A5B0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leggings", 0x11A3A998, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Crown", 0x11AB3F00, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Garb", 0x11AB42E8, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Wrap", 0x11AB46D0, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Trousers", 0x11AB4AB8, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Hood", 0x11BA8140, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Robe", 0x11BA8528, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Gloves", 0x11BA8910, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Trousers", 0x11BA8CF8, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Hood", 0x11C9C380, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Robe", 0x11C9C768, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Gloves", 0x11C9CB50, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Trousers", 0x11C9CF38, DS3ItemCategory.ARMOR), + DS3ItemData("Clandestine Coat", 0x11CB4E08, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Hat", 0x11D905C0, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Blue Robe", 0x11D909A8, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Gloves", 0x11D90D90, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Trousers", 0x11D91178, DS3ItemCategory.ARMOR), + DS3ItemData("Steel Soldier Helm", 0x12625A00, DS3ItemCategory.ARMOR), + DS3ItemData("Deserter Armor", 0x12625DE8, DS3ItemCategory.ARMOR), + DS3ItemData("Deserter Trousers", 0x126265B8, DS3ItemCategory.ARMOR), + DS3ItemData("Thief Mask", 0x12656740, DS3ItemCategory.ARMOR), + DS3ItemData("Sage's Big Hat", 0x129020C0, DS3ItemCategory.ARMOR), + DS3ItemData("Aristocrat's Mask", 0x129F6300, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Robe", 0x129F66E8, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Gloves", 0x129F6AD0, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Trousers", 0x129F6EB8, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Hood", 0x12BDE780, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Robe", 0x12BDEB68, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Wrap", 0x12BDEF50, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Skirt", 0x12BDF338, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Hat", 0x12CD29C0, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Garb", 0x12CD2DA8, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Gloves", 0x12CD3190, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Trousers", 0x12CD3578, DS3ItemCategory.ARMOR), + DS3ItemData("Thrall Hood", 0x12D4CAE0, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Hat", 0x12DC6C00, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Robe", 0x12DC6FE8, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Gloves", 0x12DC73D0, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Trousers", 0x12DC77B8, DS3ItemCategory.ARMOR), + DS3ItemData("Scholar's Robe", 0x12E41108, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Helm", 0x12EBAE40, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Armor", 0x12EBB228, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Gauntlets", 0x12EBB610, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Leggings", 0x12EBB9F8, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Helm", 0x130291A0, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Armor", 0x13029588, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Gauntlets", 0x13029970, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Leggings", 0x13029D58, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Helm", 0x13197500, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Armor", 0x131978E8, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Gauntlets", 0x13197CD0, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Leggings", 0x131980B8, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Helm", 0x1328B740, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Armor", 0x1328BB28, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Gauntlets", 0x1328BF10, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Leggings", 0x1328C2F8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Helm", 0x1337F980, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Armor", 0x1337FD68, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Gauntlets", 0x13380150, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Leggings", 0x13380538, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Mask", 0x133F9AA0, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Armor", 0x133F9E88, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Gauntlets", 0x133FA270, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Leggings", 0x133FA658, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Mask", 0x13473BC0, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Armor", 0x13473FA8, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Gauntlets", 0x13474390, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Leggings", 0x13474778, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Crown", 0x13567E00, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Armor", 0x135681E8, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Gauntlets", 0x135685D0, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Leggings", 0x135689B8, DS3ItemCategory.ARMOR), + DS3ItemData("Golden Crown", 0x1365C040, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonscale Armor", 0x1365C428, DS3ItemCategory.ARMOR), + DS3ItemData("Golden Bracelets", 0x1365C810, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonscale Waistcloth", 0x1365CBF8, DS3ItemCategory.ARMOR), + DS3ItemData("Wolnir's Crown", 0x136D6160, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Helm", 0x13750280, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Armor", 0x13750668, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Gauntlet", 0x13750A50, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Leggings", 0x13750E38, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Helm", 0x13938700, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Armor", 0x13938AE8, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Gauntlets", 0x13938ED0, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Leggings", 0x139392B8, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Helm", 0x13A2C940, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Armor", 0x13A2CD28, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Gauntlets", 0x13A2D110, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Leggings", 0x13A2D4F8, DS3ItemCategory.ARMOR), + DS3ItemData("Hood of Prayer", 0x13AA6A60, DS3ItemCategory.ARMOR), + DS3ItemData("Robe of Prayer", 0x13AA6E48, DS3ItemCategory.ARMOR), + DS3ItemData("Skirt of Prayer", 0x13AA7618, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Crown", 0x13C14DC0, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Armor", 0x13C151A8, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Gauntlets", 0x13C15590, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Leggings", 0x13C15978, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Helm", 0x13D09000, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Armor", 0x13D093E8, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Gauntlets", 0x13D097D0, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Leggings", 0x13D09BB8, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon White Crown", 0x13EF1480, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon Holy Garb", 0x13EF1868, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon Skirt", 0x13EF2038, DS3ItemCategory.ARMOR), + DS3ItemData("Deacon Robe", 0x13F6B988, DS3ItemCategory.ARMOR), + DS3ItemData("Deacon Skirt", 0x13F6C158, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Robe", 0x140D9CE8, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Gloves", 0x140DA0D0, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Skirt", 0x140DA4B8, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Helm", 0x142C1D80, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Armor", 0x142C2168, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Gauntlets", 0x142C2550, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Leggings", 0x142C2938, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Helm", 0x143B5FC0, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Armor", 0x143B63A8, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Gauntlets", 0x143B6790, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Leggings", 0x143B6B78, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Helm", 0x144AA200, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Armor", 0x144AA5E8, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Gauntlets", 0x144AA9D0, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Leggings", 0x144AADB8, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Helm", 0x1459E440, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Armor", 0x1459E828, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Gauntlets", 0x1459EC10, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Boots", 0x1459EFF8, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Helm", 0x14692680, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Armor", 0x14692A68, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Gauntlets", 0x14692E50, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Leggings", 0x14693238, DS3ItemCategory.ARMOR), + DS3ItemData("Standard Helm", 0x1470C7A0, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Armor", 0x1470CB88, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Gauntlets", 0x1470CF70, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Boots", 0x1470D358, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Helm", 0x147868C0, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Armor", 0x14786CA8, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Gauntlets", 0x14787090, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Leggings", 0x14787478, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Hood", 0x148009E0, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Armor", 0x14800DC8, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Gauntlets", 0x148011B0, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Trousers", 0x14801598, DS3ItemCategory.ARMOR), + DS3ItemData("Pharis's Hat", 0x1487AB00, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Armor", 0x1487AEE8, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Gloves", 0x1487B2D0, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Boots", 0x1487B6B8, DS3ItemCategory.ARMOR), + DS3ItemData("Ragged Mask", 0x148F4C20, DS3ItemCategory.ARMOR), + DS3ItemData("Master's Attire", 0x148F5008, DS3ItemCategory.ARMOR), + DS3ItemData("Master's Gloves", 0x148F53F0, DS3ItemCategory.ARMOR), + DS3ItemData("Loincloth", 0x148F57D8, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Hat", 0x1496ED40, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Coat", 0x1496F128, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Gauntlets", 0x1496F510, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Boots", 0x1496F8F8, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Hood", 0x149E8E60, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Robe", 0x149E9248, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Manchettes", 0x149E9630, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Boots", 0x149E9A18, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Armor", 0x14A63368, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Gloves", 0x14A63750, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Boots", 0x14A63B38, DS3ItemCategory.ARMOR), + DS3ItemData("Symbol of Avarice", 0x14ADD0A0, DS3ItemCategory.ARMOR), + DS3ItemData("Creighton's Steel Mask", 0x14B571C0, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Mail", 0x14B575A8, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Gloves", 0x14B57990, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Leggings", 0x14B57D78, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Hood", 0x14BD12E0, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Robe", 0x14BD16C8, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Gloves", 0x14BD1AB0, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Skirt", 0x14BD1E98, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Helm", 0x14C4B400, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Armor", 0x14C4B7E8, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Gauntlets", 0x14C4BBD0, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Leggings", 0x14C4BFB8, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Mask", 0x14D3F640, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Garb", 0x14D3FA28, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Gauntlets", 0x14D3FE10, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Leggings", 0x14D401F8, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Helm", 0x14E33880, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Armor", 0x14E33C68, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Gauntlets", 0x14E34050, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Leggings", 0x14E34438, DS3ItemCategory.ARMOR), + DS3ItemData("Helm of Favor", 0x14F27AC0, DS3ItemCategory.ARMOR), + DS3ItemData("Embraced Armor of Favor", 0x14F27EA8, DS3ItemCategory.ARMOR), + DS3ItemData("Gauntlets of Favor", 0x14F28290, DS3ItemCategory.ARMOR), + DS3ItemData("Leggings of Favor", 0x14F28678, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Helm", 0x1501BD00, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Armor", 0x1501C0E8, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Gauntlets", 0x1501C4D0, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Leggings", 0x1501C8B8, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Helm", 0x1510FF40, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Armor", 0x15110328, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Gauntlets", 0x15110710, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Leggings", 0x15110AF8, DS3ItemCategory.ARMOR), + DS3ItemData("Lucatiel's Mask", 0x15204180, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Vest", 0x15204568, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Gloves", 0x15204950, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Trousers", 0x15204D38, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Helm", 0x152F83C0, DS3ItemCategory.ARMOR), + DS3ItemData("Armor of the Sun", 0x152F87A8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Bracelets", 0x152F8B90, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Leggings", 0x152F8F78, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Helm", 0x153EC600, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Armor", 0x153EC9E8, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Gauntlets", 0x153ECDD0, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Leggings", 0x153ED1B8, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Armor", 0x154E0C28, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Gauntlets", 0x154E1010, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Shoes", 0x154E13F8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Helm", 0x155D4A80, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Armor", 0x155D4E68, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Gauntlets", 0x155D5250, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Leggings", 0x155D5638, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Hood", 0x156C8CC0, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Gown", 0x156C90A8, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Gloves", 0x156C9490, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Waistcloth", 0x156C9878, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Helm", 0x157BCF00, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Armor", 0x157BD2E8, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Gauntlets", 0x157BD6D0, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Leggings", 0x157BDAB8, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Helm", 0x158B1140, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Armor", 0x158B1528, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Gauntlets", 0x158B1910, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Leggings", 0x158B1CF8, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Helm", 0x159A5380, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Armor", 0x159A5768, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Gauntlets", 0x159A5B50, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Leggings", 0x159A5F38, DS3ItemCategory.ARMOR), + DS3ItemData("Helm of Thorns", 0x15B8D800, DS3ItemCategory.ARMOR), + DS3ItemData("Armor of Thorns", 0x15B8DBE8, DS3ItemCategory.ARMOR), + DS3ItemData("Gauntlets of Thorns", 0x15B8DFD0, DS3ItemCategory.ARMOR), + DS3ItemData("Leggings of Thorns", 0x15B8E3B8, DS3ItemCategory.ARMOR), + DS3ItemData("Crown of Dusk", 0x15D75C80, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Dress", 0x15D76068, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Gloves", 0x15D76450, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Skirt", 0x15D76838, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Pointed Hat", 0x15E69EC0, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Coat", 0x15E6A2A8, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Gloves", 0x15E6A690, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Trousers", 0x15E6AA78, DS3ItemCategory.ARMOR), # Covenants - ("Blade of the Darkmoon", 0x20002710, DS3ItemCategory.SKIP), - ("Watchdogs of Farron", 0x20002724, DS3ItemCategory.SKIP), - ("Aldrich Faithful", 0x2000272E, DS3ItemCategory.SKIP), - ("Warrior of Sunlight", 0x20002738, DS3ItemCategory.SKIP), - ("Mound-makers", 0x20002742, DS3ItemCategory.SKIP), - ("Way of Blue", 0x2000274C, DS3ItemCategory.SKIP), - ("Blue Sentinels", 0x20002756, DS3ItemCategory.SKIP), - ("Rosaria's Fingers", 0x20002760, DS3ItemCategory.SKIP), + DS3ItemData("Blade of the Darkmoon", 0x20002710, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Watchdogs of Farron", 0x20002724, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Aldrich Faithful", 0x2000272E, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Warrior of Sunlight", 0x20002738, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Mound-makers", 0x20002742, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Way of Blue", 0x2000274C, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Blue Sentinels", 0x20002756, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Rosaria's Fingers", 0x20002760, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Spears of the Church", 0x2000276A, DS3ItemCategory.UNIQUE, skip = True), # Rings - ("Life Ring", 0x20004E20, DS3ItemCategory.RING), - ("Life Ring+1", 0x20004E21, DS3ItemCategory.RING), - ("Life Ring+2", 0x20004E22, DS3ItemCategory.RING), - ("Life Ring+3", 0x20004E23, DS3ItemCategory.RING), - ("Chloranthy Ring", 0x20004E2A, DS3ItemCategory.RING), - ("Chloranthy Ring+1", 0x20004E2B, DS3ItemCategory.RING), - ("Chloranthy Ring+2", 0x20004E2C, DS3ItemCategory.RING), - ("Havel's Ring", 0x20004E34, DS3ItemCategory.RING), - ("Havel's Ring+1", 0x20004E35, DS3ItemCategory.RING), - ("Havel's Ring+2", 0x20004E36, DS3ItemCategory.RING), - ("Ring of Favor", 0x20004E3E, DS3ItemCategory.RING), - ("Ring of Favor+1", 0x20004E3F, DS3ItemCategory.RING), - ("Ring of Favor+2", 0x20004E40, DS3ItemCategory.RING), - ("Ring of Steel Protection", 0x20004E48, DS3ItemCategory.RING), - ("Ring of Steel Protection+1", 0x20004E49, DS3ItemCategory.RING), - ("Ring of Steel Protection+2", 0x20004E4A, DS3ItemCategory.RING), - ("Flame Stoneplate Ring", 0x20004E52, DS3ItemCategory.RING), - ("Flame Stoneplate Ring+1", 0x20004E53, DS3ItemCategory.RING), - ("Flame Stoneplate Ring+2", 0x20004E54, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring", 0x20004E5C, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring+1", 0x20004E5D, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring+2", 0x20004E5E, DS3ItemCategory.RING), - ("Magic Stoneplate Ring", 0x20004E66, DS3ItemCategory.RING), - ("Magic Stoneplate Ring+1", 0x20004E67, DS3ItemCategory.RING), - ("Magic Stoneplate Ring+2", 0x20004E68, DS3ItemCategory.RING), - ("Dark Stoneplate Ring", 0x20004E70, DS3ItemCategory.RING), - ("Dark Stoneplate Ring+1", 0x20004E71, DS3ItemCategory.RING), - ("Dark Stoneplate Ring+2", 0x20004E72, DS3ItemCategory.RING), - ("Speckled Stoneplate Ring", 0x20004E7A, DS3ItemCategory.RING), - ("Speckled Stoneplate Ring+1", 0x20004E7B, DS3ItemCategory.RING), - ("Bloodbite Ring", 0x20004E84, DS3ItemCategory.RING), - ("Bloodbite Ring+1", 0x20004E85, DS3ItemCategory.RING), - ("Poisonbite Ring", 0x20004E8E, DS3ItemCategory.RING), - ("Poisonbite Ring+1", 0x20004E8F, DS3ItemCategory.RING), - ("Cursebite Ring", 0x20004E98, DS3ItemCategory.RING), - ("Fleshbite Ring", 0x20004EA2, DS3ItemCategory.RING), - ("Fleshbite Ring+1", 0x20004EA3, DS3ItemCategory.RING), - ("Wood Grain Ring", 0x20004EAC, DS3ItemCategory.RING), - ("Wood Grain Ring+1", 0x20004EAD, DS3ItemCategory.RING), - ("Wood Grain Ring+2", 0x20004EAE, DS3ItemCategory.RING), - ("Scholar Ring", 0x20004EB6, DS3ItemCategory.RING), - ("Priestess Ring", 0x20004EC0, DS3ItemCategory.RING), - ("Red Tearstone Ring", 0x20004ECA, DS3ItemCategory.RING), - ("Blue Tearstone Ring", 0x20004ED4, DS3ItemCategory.RING), - ("Wolf Ring", 0x20004EDE, DS3ItemCategory.RING), - ("Wolf Ring+1", 0x20004EDF, DS3ItemCategory.RING), - ("Wolf Ring+2", 0x20004EE0, DS3ItemCategory.RING), - ("Leo Ring", 0x20004EE8, DS3ItemCategory.RING), - ("Ring of Sacrifice", 0x20004EF2, DS3ItemCategory.RING), - ("Young Dragon Ring", 0x20004F06, DS3ItemCategory.RING), - ("Bellowing Dragoncrest Ring", 0x20004F07, DS3ItemCategory.RING), - ("Great Swamp Ring", 0x20004F10, DS3ItemCategory.RING), - ("Witch's Ring", 0x20004F11, DS3ItemCategory.RING), - ("Morne's Ring", 0x20004F1A, DS3ItemCategory.RING), - ("Ring of the Sun's First Born", 0x20004F1B, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring", 0x20004F2E, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring+1", 0x20004F2F, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring+2", 0x20004F30, DS3ItemCategory.RING), - ("Sage Ring", 0x20004F38, DS3ItemCategory.RING), - ("Sage Ring+1", 0x20004F39, DS3ItemCategory.RING), - ("Sage Ring+2", 0x20004F3A, DS3ItemCategory.RING), - ("Slumbering Dragoncrest Ring", 0x20004F42, DS3ItemCategory.RING), - ("Dusk Crown Ring", 0x20004F4C, DS3ItemCategory.RING), - ("Saint's Ring", 0x20004F56, DS3ItemCategory.RING), - ("Deep Ring", 0x20004F60, DS3ItemCategory.RING), - ("Darkmoon Ring", 0x20004F6A, DS3ItemCategory.RING), - ("Hawk Ring", 0x20004F92, DS3ItemCategory.RING), - ("Hornet Ring", 0x20004F9C, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring", 0x20004FA6, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+1", 0x20004FA7, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+2", 0x20004FA8, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring", 0x20004FB0, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+1", 0x20004FB1, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+2", 0x20004FB2, DS3ItemCategory.RING), - ("Sun Princess Ring", 0x20004FBA, DS3ItemCategory.RING), - ("Silvercat Ring", 0x20004FC4, DS3ItemCategory.RING), - ("Skull Ring", 0x20004FCE, DS3ItemCategory.RING), - ("Untrue White Ring", 0x20004FD8, DS3ItemCategory.RING), - ("Carthus Milkring", 0x20004FE2, DS3ItemCategory.RING), - ("Knight's Ring", 0x20004FEC, DS3ItemCategory.RING), - ("Hunter's Ring", 0x20004FF6, DS3ItemCategory.RING), - ("Knight Slayer's Ring", 0x20005000, DS3ItemCategory.RING), - ("Magic Clutch Ring", 0x2000500A, DS3ItemCategory.RING), - ("Lightning Clutch Ring", 0x20005014, DS3ItemCategory.RING), - ("Fire Clutch Ring", 0x2000501E, DS3ItemCategory.RING), - ("Dark Clutch Ring", 0x20005028, DS3ItemCategory.RING), - ("Flynn's Ring", 0x2000503C, DS3ItemCategory.RING), - ("Prisoner's Chain", 0x20005046, DS3ItemCategory.RING), - ("Untrue Dark Ring", 0x20005050, DS3ItemCategory.RING), - ("Obscuring Ring", 0x20005064, DS3ItemCategory.RING), - ("Ring of the Evil Eye", 0x2000506E, DS3ItemCategory.RING), - ("Ring of the Evil Eye+1", 0x2000506F, DS3ItemCategory.RING), - ("Ring of the Evil Eye+2", 0x20005070, DS3ItemCategory.RING), - ("Calamity Ring", 0x20005078, DS3ItemCategory.RING), - ("Farron Ring", 0x20005082, DS3ItemCategory.RING), - ("Aldrich's Ruby", 0x2000508C, DS3ItemCategory.RING), - ("Aldrich's Sapphire", 0x20005096, DS3ItemCategory.RING), - ("Lloyd's Sword Ring", 0x200050B4, DS3ItemCategory.RING), - ("Lloyd's Shield Ring", 0x200050BE, DS3ItemCategory.RING), - ("Estus Ring", 0x200050DC, DS3ItemCategory.RING), - ("Ashen Estus Ring", 0x200050E6, DS3ItemCategory.RING), - ("Carthus Bloodring", 0x200050FA, DS3ItemCategory.RING), - ("Reversal Ring", 0x20005104, DS3ItemCategory.RING), - ("Pontiff's Right Eye", 0x2000510E, DS3ItemCategory.RING), - ("Pontiff's Left Eye", 0x20005136, DS3ItemCategory.RING), - ("Dragonscale Ring", 0x2000515E, DS3ItemCategory.RING), + DS3ItemData("Life Ring", 0x20004E20, DS3ItemCategory.RING), + DS3ItemData("Life Ring+1", 0x20004E21, DS3ItemCategory.RING), + DS3ItemData("Life Ring+2", 0x20004E22, DS3ItemCategory.RING), + DS3ItemData("Life Ring+3", 0x20004E23, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring", 0x20004E2A, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Chloranthy Ring+1", 0x20004E2B, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring+2", 0x20004E2C, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Havel's Ring", 0x20004E34, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Havel's Ring+1", 0x20004E35, DS3ItemCategory.RING), + DS3ItemData("Havel's Ring+2", 0x20004E36, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Ring of Favor", 0x20004E3E, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Ring of Favor+1", 0x20004E3F, DS3ItemCategory.RING), + DS3ItemData("Ring of Favor+2", 0x20004E40, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Ring of Steel Protection", 0x20004E48, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Ring of Steel Protection+1", 0x20004E49, DS3ItemCategory.RING), + DS3ItemData("Ring of Steel Protection+2", 0x20004E4A, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Flame Stoneplate Ring", 0x20004E52, DS3ItemCategory.RING), + DS3ItemData("Flame Stoneplate Ring+1", 0x20004E53, DS3ItemCategory.RING), + DS3ItemData("Flame Stoneplate Ring+2", 0x20004E54, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring", 0x20004E5C, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring+1", 0x20004E5D, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring+2", 0x20004E5E, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring", 0x20004E66, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring+1", 0x20004E67, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring+2", 0x20004E68, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring", 0x20004E70, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring+1", 0x20004E71, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring+2", 0x20004E72, DS3ItemCategory.RING), + DS3ItemData("Speckled Stoneplate Ring", 0x20004E7A, DS3ItemCategory.RING), + DS3ItemData("Speckled Stoneplate Ring+1", 0x20004E7B, DS3ItemCategory.RING), + DS3ItemData("Bloodbite Ring", 0x20004E84, DS3ItemCategory.RING), + DS3ItemData("Bloodbite Ring+1", 0x20004E85, DS3ItemCategory.RING), + DS3ItemData("Poisonbite Ring", 0x20004E8E, DS3ItemCategory.RING), + DS3ItemData("Poisonbite Ring+1", 0x20004E8F, DS3ItemCategory.RING), + DS3ItemData("Cursebite Ring", 0x20004E98, DS3ItemCategory.RING), + DS3ItemData("Fleshbite Ring", 0x20004EA2, DS3ItemCategory.RING), + DS3ItemData("Fleshbite Ring+1", 0x20004EA3, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring", 0x20004EAC, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring+1", 0x20004EAD, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring+2", 0x20004EAE, DS3ItemCategory.RING), + DS3ItemData("Scholar Ring", 0x20004EB6, DS3ItemCategory.RING), + DS3ItemData("Priestess Ring", 0x20004EC0, DS3ItemCategory.RING), + DS3ItemData("Red Tearstone Ring", 0x20004ECA, DS3ItemCategory.RING), + DS3ItemData("Blue Tearstone Ring", 0x20004ED4, DS3ItemCategory.RING), + DS3ItemData("Wolf Ring", 0x20004EDE, DS3ItemCategory.RING, + inject = True), # Covenant reward + DS3ItemData("Wolf Ring+1", 0x20004EDF, DS3ItemCategory.RING), + DS3ItemData("Wolf Ring+2", 0x20004EE0, DS3ItemCategory.RING), + DS3ItemData("Leo Ring", 0x20004EE8, DS3ItemCategory.RING), + DS3ItemData("Ring of Sacrifice", 0x20004EF2, DS3ItemCategory.RING, filler = True), + DS3ItemData("Young Dragon Ring", 0x20004F06, DS3ItemCategory.RING), + DS3ItemData("Bellowing Dragoncrest Ring", 0x20004F07, DS3ItemCategory.RING), + DS3ItemData("Great Swamp Ring", 0x20004F10, DS3ItemCategory.RING), + DS3ItemData("Witch's Ring", 0x20004F11, DS3ItemCategory.RING), + DS3ItemData("Morne's Ring", 0x20004F1A, DS3ItemCategory.RING), + DS3ItemData("Ring of the Sun's First Born", 0x20004F1B, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring", 0x20004F2E, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring+1", 0x20004F2F, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring+2", 0x20004F30, DS3ItemCategory.RING), + DS3ItemData("Sage Ring", 0x20004F38, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_NGP), + DS3ItemData("Sage Ring+1", 0x20004F39, DS3ItemCategory.RING), + DS3ItemData("Sage Ring+2", 0x20004F3A, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Slumbering Dragoncrest Ring", 0x20004F42, DS3ItemCategory.RING), + DS3ItemData("Dusk Crown Ring", 0x20004F4C, DS3ItemCategory.RING), + DS3ItemData("Saint's Ring", 0x20004F56, DS3ItemCategory.RING), + DS3ItemData("Deep Ring", 0x20004F60, DS3ItemCategory.RING), + DS3ItemData("Darkmoon Ring", 0x20004F6A, DS3ItemCategory.RING, + inject = True), # Covenant reward + DS3ItemData("Hawk Ring", 0x20004F92, DS3ItemCategory.RING), + DS3ItemData("Hornet Ring", 0x20004F9C, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring", 0x20004FA6, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+1", 0x20004FA7, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+2", 0x20004FA8, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring", 0x20004FB0, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Covetous Silver Serpent Ring+1", 0x20004FB1, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring+2", 0x20004FB2, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Sun Princess Ring", 0x20004FBA, DS3ItemCategory.RING), + DS3ItemData("Silvercat Ring", 0x20004FC4, DS3ItemCategory.RING), + DS3ItemData("Skull Ring", 0x20004FCE, DS3ItemCategory.RING), + DS3ItemData("Untrue White Ring", 0x20004FD8, DS3ItemCategory.RING, skip = True), + DS3ItemData("Carthus Milkring", 0x20004FE2, DS3ItemCategory.RING), + DS3ItemData("Knight's Ring", 0x20004FEC, DS3ItemCategory.RING), + DS3ItemData("Hunter's Ring", 0x20004FF6, DS3ItemCategory.RING), + DS3ItemData("Knight Slayer's Ring", 0x20005000, DS3ItemCategory.RING), + DS3ItemData("Magic Clutch Ring", 0x2000500A, DS3ItemCategory.RING), + DS3ItemData("Lightning Clutch Ring", 0x20005014, DS3ItemCategory.RING), + DS3ItemData("Fire Clutch Ring", 0x2000501E, DS3ItemCategory.RING), + DS3ItemData("Dark Clutch Ring", 0x20005028, DS3ItemCategory.RING), + DS3ItemData("Flynn's Ring", 0x2000503C, DS3ItemCategory.RING), + DS3ItemData("Prisoner's Chain", 0x20005046, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Untrue Dark Ring", 0x20005050, DS3ItemCategory.RING), + DS3ItemData("Obscuring Ring", 0x20005064, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye", 0x2000506E, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye+1", 0x2000506F, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye+2", 0x20005070, DS3ItemCategory.RING), + DS3ItemData("Calamity Ring", 0x20005078, DS3ItemCategory.RING), + DS3ItemData("Farron Ring", 0x20005082, DS3ItemCategory.RING), + DS3ItemData("Aldrich's Ruby", 0x2000508C, DS3ItemCategory.RING), + DS3ItemData("Aldrich's Sapphire", 0x20005096, DS3ItemCategory.RING), + DS3ItemData("Lloyd's Sword Ring", 0x200050B4, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Lloyd's Shield Ring", 0x200050BE, DS3ItemCategory.RING), + DS3ItemData("Estus Ring", 0x200050DC, DS3ItemCategory.RING), + DS3ItemData("Ashen Estus Ring", 0x200050E6, DS3ItemCategory.RING), + DS3ItemData("Horsehoof Ring", 0x200050F0, DS3ItemCategory.RING), + DS3ItemData("Carthus Bloodring", 0x200050FA, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Reversal Ring", 0x20005104, DS3ItemCategory.RING), + DS3ItemData("Pontiff's Right Eye", 0x2000510E, DS3ItemCategory.RING), + DS3ItemData("Pontiff's Left Eye", 0x20005136, DS3ItemCategory.RING), + DS3ItemData("Dragonscale Ring", 0x2000515E, DS3ItemCategory.RING), # Items - ("Roster of Knights", 0x4000006C, DS3ItemCategory.SKIP), - ("Cracked Red Eye Orb", 0x4000006F, DS3ItemCategory.SKIP), - ("Divine Blessing", 0x400000F0, DS3ItemCategory.MISC), - ("Hidden Blessing", 0x400000F1, DS3ItemCategory.MISC), - ("Silver Pendant", 0x400000F2, DS3ItemCategory.SKIP), - ("Green Blossom", 0x40000104, DS3ItemCategory.MISC), - ("Budding Green Blossom", 0x40000106, DS3ItemCategory.MISC), - ("Bloodred Moss Clump", 0x4000010E, DS3ItemCategory.SKIP), - ("Purple Moss Clump", 0x4000010F, DS3ItemCategory.MISC), - ("Blooming Purple Moss Clump", 0x40000110, DS3ItemCategory.SKIP), - ("Purging Stone", 0x40000112, DS3ItemCategory.SKIP), - ("Rime-blue Moss Clump", 0x40000114, DS3ItemCategory.SKIP), - ("Repair Powder", 0x40000118, DS3ItemCategory.MISC), - ("Kukri", 0x40000122, DS3ItemCategory.SKIP), - ("Firebomb", 0x40000124, DS3ItemCategory.MISC), - ("Dung Pie", 0x40000125, DS3ItemCategory.SKIP), - ("Alluring Skull", 0x40000126, DS3ItemCategory.MISC), - ("Undead Hunter Charm", 0x40000128, DS3ItemCategory.MISC), - ("Black Firebomb", 0x40000129, DS3ItemCategory.MISC), - ("Rope Firebomb", 0x4000012B, DS3ItemCategory.MISC), - ("Lightning Urn", 0x4000012C, DS3ItemCategory.MISC), - ("Rope Black Firebomb", 0x4000012E, DS3ItemCategory.MISC), - ("Stalk Dung Pie", 0x4000012F, DS3ItemCategory.SKIP), - ("Duel Charm", 0x40000130, DS3ItemCategory.MISC), - ("Throwing Knife", 0x40000136, DS3ItemCategory.MISC), - ("Poison Throwing Knife", 0x40000137, DS3ItemCategory.MISC), - ("Charcoal Pine Resin", 0x4000014A, DS3ItemCategory.MISC), - ("Gold Pine Resin", 0x4000014B, DS3ItemCategory.MISC), - ("Human Pine Resin", 0x4000014E, DS3ItemCategory.MISC), - ("Carthus Rouge", 0x4000014F, DS3ItemCategory.MISC), - ("Pale Pine Resin", 0x40000150, DS3ItemCategory.MISC), - ("Charcoal Pine Bundle", 0x40000154, DS3ItemCategory.MISC), - ("Gold Pine Bundle", 0x40000155, DS3ItemCategory.MISC), - ("Rotten Pine Resin", 0x40000157, DS3ItemCategory.MISC), - ("Homeward Bone", 0x4000015E, DS3ItemCategory.MISC), - ("Coiled Sword Fragment", 0x4000015F, DS3ItemCategory.MISC), - ("Wolf's Blood Swordgrass", 0x4000016E, DS3ItemCategory.MISC), - ("Human Dregs", 0x4000016F, DS3ItemCategory.SKIP), - ("Forked Pale Tongue", 0x40000170, DS3ItemCategory.MISC), - ("Proof of a Concord Well Kept", 0x40000171, DS3ItemCategory.SKIP), - ("Prism Stone", 0x40000172, DS3ItemCategory.SKIP), - ("Binoculars", 0x40000173, DS3ItemCategory.MISC), - ("Proof of a Concord Kept", 0x40000174, DS3ItemCategory.SKIP), - ("Pale Tongue", 0x40000175, DS3ItemCategory.MISC), - ("Vertebra Shackle", 0x40000176, DS3ItemCategory.SKIP), - ("Sunlight Medal", 0x40000177, DS3ItemCategory.SKIP), - ("Dragon Head Stone", 0x40000179, DS3ItemCategory.MISC), - ("Dragon Torso Stone", 0x4000017A, DS3ItemCategory.MISC), - ("Rubbish", 0x4000017C, DS3ItemCategory.SKIP), - ("Dried Finger", 0x40000181, DS3ItemCategory.SKIP), - ("Twinkling Dragon Head Stone", 0x40000183, DS3ItemCategory.MISC), - ("Twinkling Dragon Torso Stone", 0x40000184, DS3ItemCategory.MISC), - ("Fire Keeper Soul", 0x40000186, DS3ItemCategory.MISC), - ("Fading Soul", 0x40000190, DS3ItemCategory.MISC), - ("Soul of a Deserted Corpse", 0x40000191, DS3ItemCategory.MISC), - ("Large Soul of a Deserted Corpse", 0x40000192, DS3ItemCategory.MISC), - ("Soul of an Unknown Traveler", 0x40000193, DS3ItemCategory.MISC), - ("Large Soul of an Unknown Traveler", 0x40000194, DS3ItemCategory.MISC), - ("Soul of a Nameless Soldier", 0x40000195, DS3ItemCategory.MISC), - ("Large Soul of a Nameless Soldier", 0x40000196, DS3ItemCategory.MISC), - ("Soul of a Weary Warrior", 0x40000197, DS3ItemCategory.MISC), - ("Large Soul of a Weary Warrior", 0x40000198, DS3ItemCategory.MISC), - ("Soul of a Crestfallen Knight", 0x40000199, DS3ItemCategory.MISC), - ("Large Soul of a Crestfallen Knight", 0x4000019A, DS3ItemCategory.MISC), - ("Soul of a Proud Paladin", 0x4000019B, DS3ItemCategory.MISC), - ("Large Soul of a Proud Paladin", 0x4000019C, DS3ItemCategory.MISC), - ("Soul of an Intrepid Hero", 0x4000019D, DS3ItemCategory.MISC), - ("Large Soul of an Intrepid Hero", 0x4000019E, DS3ItemCategory.MISC), - ("Soul of a Seasoned Warrior", 0x4000019F, DS3ItemCategory.MISC), - ("Large Soul of a Seasoned Warrior", 0x400001A0, DS3ItemCategory.MISC), - ("Soul of an Old Hand", 0x400001A1, DS3ItemCategory.MISC), - ("Soul of a Venerable Old Hand", 0x400001A2, DS3ItemCategory.MISC), - ("Soul of a Champion", 0x400001A3, DS3ItemCategory.MISC), - ("Soul of a Great Champion", 0x400001A4, DS3ItemCategory.MISC), - ("Seed of a Giant Tree", 0x400001B8, DS3ItemCategory.SKIP), - ("Young White Branch", 0x400001C6, DS3ItemCategory.SKIP), - ("Rusted Coin", 0x400001C7, DS3ItemCategory.MISC), - ("Siegbräu", 0x400001C8, DS3ItemCategory.SKIP), - ("Rusted Gold Coin", 0x400001C9, DS3ItemCategory.MISC), - ("Blue Bug Pellet", 0x400001CA, DS3ItemCategory.SKIP), - ("Red Bug Pellet", 0x400001CB, DS3ItemCategory.SKIP), - ("Yellow Bug Pellet", 0x400001CC, DS3ItemCategory.SKIP), - ("Black Bug Pellet", 0x400001CD, DS3ItemCategory.SKIP), - ("Young White Branch", 0x400001CF, DS3ItemCategory.SKIP), - ("Dark Sigil", 0x400001EA, DS3ItemCategory.SKIP), - ("Ember", 0x400001F4, DS3ItemCategory.MISC), - ("Soul of Champion Gundyr", 0x400002C8, DS3ItemCategory.BOSS), - ("Soul of the Dancer", 0x400002CA, DS3ItemCategory.BOSS), - ("Soul of a Crystal Sage", 0x400002CB, DS3ItemCategory.BOSS), - ("Soul of the Blood of the Wolf", 0x400002CD, DS3ItemCategory.BOSS), - ("Soul of Consumed Oceiros", 0x400002CE, DS3ItemCategory.BOSS), - ("Soul of Boreal Valley Vordt", 0x400002CF, DS3ItemCategory.BOSS), - ("Soul of the Old Demon King", 0x400002D0, DS3ItemCategory.BOSS), - ("Soul of Dragonslayer Armour", 0x400002D1, DS3ItemCategory.BOSS), - ("Soul of the Nameless King", 0x400002D2, DS3ItemCategory.BOSS), - ("Soul of Pontiff Sulyvahn", 0x400002D4, DS3ItemCategory.BOSS), - ("Soul of Aldrich", 0x400002D5, DS3ItemCategory.BOSS), - ("Soul of High Lord Wolnir", 0x400002D6, DS3ItemCategory.BOSS), - ("Soul of the Rotted Greatwood", 0x400002D7, DS3ItemCategory.BOSS), - ("Soul of Rosaria", 0x400002D8, DS3ItemCategory.MISC), - ("Soul of the Deacons of the Deep", 0x400002D9, DS3ItemCategory.BOSS), - ("Soul of the Twin Princes", 0x400002DB, DS3ItemCategory.BOSS), - ("Soul of Yhorm the Giant", 0x400002DC, DS3ItemCategory.BOSS), - ("Soul of the Lords", 0x400002DD, DS3ItemCategory.MISC), - ("Soul of a Demon", 0x400002E3, DS3ItemCategory.BOSS), - ("Soul of a Stray Demon", 0x400002E7, DS3ItemCategory.BOSS), - ("Titanite Shard", 0x400003E8, DS3ItemCategory.MISC), - ("Large Titanite Shard", 0x400003E9, DS3ItemCategory.MISC), - ("Titanite Chunk", 0x400003EA, DS3ItemCategory.MISC), - ("Titanite Slab", 0x400003EB, DS3ItemCategory.MISC), - ("Titanite Scale", 0x400003FC, DS3ItemCategory.MISC), - ("Twinkling Titanite", 0x40000406, DS3ItemCategory.MISC), - ("Heavy Gem", 0x4000044C, DS3ItemCategory.MISC), - ("Sharp Gem", 0x40000456, DS3ItemCategory.MISC), - ("Refined Gem", 0x40000460, DS3ItemCategory.MISC), - ("Crystal Gem", 0x4000046A, DS3ItemCategory.MISC), - ("Simple Gem", 0x40000474, DS3ItemCategory.MISC), - ("Fire Gem", 0x4000047E, DS3ItemCategory.MISC), - ("Chaos Gem", 0x40000488, DS3ItemCategory.MISC), - ("Lightning Gem", 0x40000492, DS3ItemCategory.MISC), - ("Deep Gem", 0x4000049C, DS3ItemCategory.MISC), - ("Dark Gem", 0x400004A6, DS3ItemCategory.MISC), - ("Poison Gem", 0x400004B0, DS3ItemCategory.MISC), - ("Blood Gem", 0x400004BA, DS3ItemCategory.MISC), - ("Raw Gem", 0x400004C4, DS3ItemCategory.MISC), - ("Blessed Gem", 0x400004CE, DS3ItemCategory.MISC), - ("Hollow Gem", 0x400004D8, DS3ItemCategory.MISC), - ("Shriving Stone", 0x400004E2, DS3ItemCategory.MISC), - ("Lift Chamber Key", 0x400007D1, DS3ItemCategory.KEY), - ("Small Doll", 0x400007D5, DS3ItemCategory.KEY), - ("Jailbreaker's Key", 0x400007D7, DS3ItemCategory.KEY), - ("Jailer's Key Ring", 0x400007D8, DS3ItemCategory.KEY), - ("Grave Key", 0x400007D9, DS3ItemCategory.KEY), - ("Cell Key", 0x400007DA, DS3ItemCategory.KEY), - ("Dungeon Ground Floor Key", 0x400007DB, DS3ItemCategory.KEY), - ("Old Cell Key", 0x400007DC, DS3ItemCategory.KEY), - ("Grand Archives Key", 0x400007DE, DS3ItemCategory.KEY), - ("Tower Key", 0x400007DF, DS3ItemCategory.KEY), - ("Small Lothric Banner", 0x40000836, DS3ItemCategory.KEY), - ("Farron Coal", 0x40000837, DS3ItemCategory.MISC), - ("Sage's Coal", 0x40000838, DS3ItemCategory.MISC), - ("Giant's Coal", 0x40000839, DS3ItemCategory.MISC), - ("Profaned Coal", 0x4000083A, DS3ItemCategory.MISC), - ("Mortician's Ashes", 0x4000083B, DS3ItemCategory.MISC), - ("Dreamchaser's Ashes", 0x4000083C, DS3ItemCategory.MISC), - ("Paladin's Ashes", 0x4000083D, DS3ItemCategory.MISC), - ("Grave Warden's Ashes", 0x4000083E, DS3ItemCategory.MISC), - ("Greirat's Ashes", 0x4000083F, DS3ItemCategory.MISC), - ("Orbeck's Ashes", 0x40000840, DS3ItemCategory.MISC), - ("Cornyx's Ashes", 0x40000841, DS3ItemCategory.MISC), - ("Karla's Ashes", 0x40000842, DS3ItemCategory.MISC), - ("Irina's Ashes", 0x40000843, DS3ItemCategory.MISC), - ("Yuria's Ashes", 0x40000844, DS3ItemCategory.MISC), - ("Basin of Vows", 0x40000845, DS3ItemCategory.KEY), - ("Loretta's Bone", 0x40000846, DS3ItemCategory.KEY), - ("Braille Divine Tome of Carim", 0x40000847, DS3ItemCategory.MISC), - ("Braille Divine Tome of Lothric", 0x40000848, DS3ItemCategory.MISC), - ("Cinders of a Lord - Abyss Watcher", 0x4000084B, DS3ItemCategory.KEY), - ("Cinders of a Lord - Aldrich", 0x4000084C, DS3ItemCategory.KEY), - ("Cinders of a Lord - Yhorm the Giant", 0x4000084D, DS3ItemCategory.KEY), - ("Cinders of a Lord - Lothric Prince", 0x4000084E, DS3ItemCategory.KEY), - ("Great Swamp Pyromancy Tome", 0x4000084F, DS3ItemCategory.MISC), - ("Carthus Pyromancy Tome", 0x40000850, DS3ItemCategory.MISC), - ("Izalith Pyromancy Tome", 0x40000851, DS3ItemCategory.MISC), - ("Quelana Pyromancy Tome", 0x40000852, DS3ItemCategory.MISC), - ("Grave Warden Pyromancy Tome", 0x40000853, DS3ItemCategory.MISC), - ("Sage's Scroll", 0x40000854, DS3ItemCategory.MISC), - ("Logan's Scroll", 0x40000855, DS3ItemCategory.MISC), - ("Crystal Scroll", 0x40000856, DS3ItemCategory.MISC), - ("Transposing Kiln", 0x40000857, DS3ItemCategory.MISC), - ("Coiled Sword", 0x40000859, DS3ItemCategory.SKIP), # Useless - ("Eyes of a Fire Keeper", 0x4000085A, DS3ItemCategory.KEY), - ("Sword of Avowal", 0x4000085B, DS3ItemCategory.KEY), - ("Golden Scroll", 0x4000085C, DS3ItemCategory.MISC), - ("Estus Shard", 0x4000085D, DS3ItemCategory.MISC), - ("Hawkwood's Swordgrass", 0x4000085E, DS3ItemCategory.SKIP), - ("Undead Bone Shard", 0x4000085F, DS3ItemCategory.MISC), - ("Deep Braille Divine Tome", 0x40000860, DS3ItemCategory.MISC), - ("Londor Braille Divine Tome", 0x40000861, DS3ItemCategory.MISC), - ("Excrement-covered Ashes", 0x40000862, DS3ItemCategory.MISC), - ("Prisoner Chief's Ashes", 0x40000863, DS3ItemCategory.MISC), - ("Xanthous Ashes", 0x40000864, DS3ItemCategory.MISC), - ("Hollow's Ashes", 0x40000865, DS3ItemCategory.MISC), - ("Patches' Ashes", 0x40000866, DS3ItemCategory.MISC), - ("Dragon Chaser's Ashes", 0x40000867, DS3ItemCategory.MISC), - ("Easterner's Ashes", 0x40000868, DS3ItemCategory.MISC), + DS3ItemData("White Sign Soapstone", 0x40000064, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Red Sign Soapstone", 0x40000066, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Red Eye Orb", 0x40000066, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Roster of Knights", 0x4000006C, DS3ItemCategory.UNIQUE, skip = True), + *DS3ItemData("Cracked Red Eye Orb", 0x4000006F, DS3ItemCategory.MISC, skip = True).counts([5]), + DS3ItemData("Black Eye Orb", 0x40000073, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Divine Blessing", 0x400000F0, DS3ItemCategory.MISC), + DS3ItemData("Hidden Blessing", 0x400000F1, DS3ItemCategory.MISC), + *DS3ItemData("Green Blossom", 0x40000104, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Budding Green Blossom", 0x40000106, DS3ItemCategory.MISC).counts([2, 3]), + *DS3ItemData("Bloodred Moss Clump", 0x4000010E, DS3ItemCategory.MISC, filler = True).counts([3]), + *DS3ItemData("Purple Moss Clump", 0x4000010F, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Blooming Purple Moss Clump", 0x40000110, DS3ItemCategory.MISC).counts([3]), + *DS3ItemData("Purging Stone", 0x40000112, DS3ItemCategory.MISC, skip = True).counts([2, 3]), + *DS3ItemData("Rime-blue Moss Clump", 0x40000114, DS3ItemCategory.MISC, filler = True).counts([2, 4]), + *DS3ItemData("Repair Powder", 0x40000118, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Kukri", 0x40000122, DS3ItemCategory.MISC).counts([8, 9]), + DS3ItemData("Kukri x5", 0x40000122, DS3ItemCategory.MISC, count = 5, filler = True), + *DS3ItemData("Firebomb", 0x40000124, DS3ItemCategory.MISC).counts([3, 5, 6]), + DS3ItemData("Firebomb x2", 0x40000124, DS3ItemCategory.MISC, count = 2, filler = True), + *DS3ItemData("Dung Pie", 0x40000125, DS3ItemCategory.MISC).counts([2, 4]), + DS3ItemData("Dung Pie x3", 0x40000125, DS3ItemCategory.MISC, count = 3, filler = True), + *DS3ItemData("Alluring Skull", 0x40000126, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Undead Hunter Charm", 0x40000128, DS3ItemCategory.MISC).counts([2, 3]), + *DS3ItemData("Black Firebomb", 0x40000129, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + DS3ItemData("Rope Firebomb", 0x4000012B, DS3ItemCategory.MISC), + *DS3ItemData("Lightning Urn", 0x4000012C, DS3ItemCategory.MISC, filler = True).counts([3, 4, 6]), + DS3ItemData("Rope Black Firebomb", 0x4000012E, DS3ItemCategory.MISC), + *DS3ItemData("Stalk Dung Pie", 0x4000012F, DS3ItemCategory.MISC).counts([6]), + *DS3ItemData("Duel Charm", 0x40000130, DS3ItemCategory.MISC).counts([3]), + *DS3ItemData("Throwing Knife", 0x40000136, DS3ItemCategory.MISC).counts([6, 8]), + DS3ItemData("Throwing Knife x5", 0x40000136, DS3ItemCategory.MISC, count = 5, filler = True), + DS3ItemData("Poison Throwing Knife", 0x40000137, DS3ItemCategory.MISC), + *DS3ItemData("Charcoal Pine Resin", 0x4000014A, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Gold Pine Resin", 0x4000014B, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Human Pine Resin", 0x4000014E, DS3ItemCategory.MISC, filler = True).counts([2, 4]), + *DS3ItemData("Carthus Rouge", 0x4000014F, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Pale Pine Resin", 0x40000150, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Charcoal Pine Bundle", 0x40000154, DS3ItemCategory.MISC).counts([2]), + *DS3ItemData("Gold Pine Bundle", 0x40000155, DS3ItemCategory.MISC).counts([6]), + *DS3ItemData("Rotten Pine Resin", 0x40000157, DS3ItemCategory.MISC).counts([2, 4]), + *DS3ItemData("Homeward Bone", 0x4000015E, DS3ItemCategory.MISC, filler = True).counts([2, 3, 6]), + DS3ItemData("Coiled Sword Fragment", 0x4000015F, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Wolf's Blood Swordgrass", 0x4000016E, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Human Dregs", 0x4000016F, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Forked Pale Tongue", 0x40000170, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Proof of a Concord Well Kept", 0x40000171, DS3ItemCategory.MISC, skip = True), + *DS3ItemData("Prism Stone", 0x40000172, DS3ItemCategory.MISC, skip = True).counts([4, 6, 10]), + DS3ItemData("Binoculars", 0x40000173, DS3ItemCategory.MISC), + DS3ItemData("Proof of a Concord Kept", 0x40000174, DS3ItemCategory.MISC, skip = True), + # One is needed for Leonhard's quest, others are useful for restatting. + DS3ItemData("Pale Tongue", 0x40000175, DS3ItemCategory.MISC, + classification = ItemClassification.progression), + DS3ItemData("Vertebra Shackle", 0x40000176, DS3ItemCategory.MISC, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Sunlight Medal", 0x40000177, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dragon Head Stone", 0x40000179, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon Torso Stone", 0x4000017A, DS3ItemCategory.UNIQUE), + DS3ItemData("Rubbish", 0x4000017C, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dried Finger", 0x40000181, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Twinkling Dragon Head Stone", 0x40000183, DS3ItemCategory.UNIQUE), + DS3ItemData("Twinkling Dragon Torso Stone", 0x40000184, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Fire Keeper Soul", 0x40000186, DS3ItemCategory.UNIQUE), + # Allow souls up to 2k in value to be used as filler + DS3ItemData("Fading Soul", 0x40000190, DS3ItemCategory.SOUL, souls = 50), + DS3ItemData("Soul of a Deserted Corpse", 0x40000191, DS3ItemCategory.SOUL, souls = 200), + DS3ItemData("Large Soul of a Deserted Corpse", 0x40000192, DS3ItemCategory.SOUL, souls = 400), + DS3ItemData("Soul of an Unknown Traveler", 0x40000193, DS3ItemCategory.SOUL, souls = 800), + DS3ItemData("Large Soul of an Unknown Traveler", 0x40000194, DS3ItemCategory.SOUL, souls = 1000), + DS3ItemData("Soul of a Nameless Soldier", 0x40000195, DS3ItemCategory.SOUL, souls = 2000), + DS3ItemData("Large Soul of a Nameless Soldier", 0x40000196, DS3ItemCategory.SOUL, souls = 3000), + DS3ItemData("Soul of a Weary Warrior", 0x40000197, DS3ItemCategory.SOUL, souls = 5000), + DS3ItemData("Large Soul of a Weary Warrior", 0x40000198, DS3ItemCategory.SOUL, souls = 8000), + DS3ItemData("Soul of a Crestfallen Knight", 0x40000199, DS3ItemCategory.SOUL, souls = 10000), + DS3ItemData("Large Soul of a Crestfallen Knight", 0x4000019A, DS3ItemCategory.SOUL, souls = 20000), + DS3ItemData("Soul of a Proud Paladin", 0x4000019B, DS3ItemCategory.SOUL, souls = 500), + DS3ItemData("Large Soul of a Proud Paladin", 0x4000019C, DS3ItemCategory.SOUL, souls = 1000), + DS3ItemData("Soul of an Intrepid Hero", 0x4000019D, DS3ItemCategory.SOUL, souls = 2000), + DS3ItemData("Large Soul of an Intrepid Hero", 0x4000019E, DS3ItemCategory.SOUL, souls = 2500), + DS3ItemData("Soul of a Seasoned Warrior", 0x4000019F, DS3ItemCategory.SOUL, souls = 5000), + DS3ItemData("Large Soul of a Seasoned Warrior", 0x400001A0, DS3ItemCategory.SOUL, souls = 7500), + DS3ItemData("Soul of an Old Hand", 0x400001A1, DS3ItemCategory.SOUL, souls = 12500), + DS3ItemData("Soul of a Venerable Old Hand", 0x400001A2, DS3ItemCategory.SOUL, souls = 20000), + DS3ItemData("Soul of a Champion", 0x400001A3, DS3ItemCategory.SOUL, souls = 25000), + DS3ItemData("Soul of a Great Champion", 0x400001A4, DS3ItemCategory.SOUL, souls = 50000), + DS3ItemData("Seed of a Giant Tree", 0x400001B8, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression, inject = True), # Crow trade + *DS3ItemData("Mossfruit", 0x400001C4, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Young White Branch", 0x400001C6, DS3ItemCategory.MISC), + *DS3ItemData("Rusted Coin", 0x400001C7, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Siegbräu", 0x400001C8, DS3ItemCategory.MISC, + classification = ItemClassification.progression), # Crow trade + *DS3ItemData("Rusted Gold Coin", 0x400001C9, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Blue Bug Pellet", 0x400001CA, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Red Bug Pellet", 0x400001CB, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Yellow Bug Pellet", 0x400001CC, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Black Bug Pellet", 0x400001CD, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + DS3ItemData("Young White Branch", 0x400001CF, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dark Sigil", 0x400001EA, DS3ItemCategory.MISC, skip = True), + *DS3ItemData("Ember", 0x400001F4, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Hello Carving", 0x40000208, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Thank you Carving", 0x40000209, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Very good! Carving", 0x4000020A, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("I'm sorry Carving", 0x4000020B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Help me! Carving", 0x4000020C, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Soul of Champion Gundyr", 0x400002C8, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Dancer", 0x400002CA, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Crystal Sage", 0x400002CB, DS3ItemCategory.BOSS, souls = 3000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Blood of the Wolf", 0x400002CD, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Consumed Oceiros", 0x400002CE, DS3ItemCategory.BOSS, souls = 12000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Boreal Valley Vordt", 0x400002CF, DS3ItemCategory.BOSS, souls = 2000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Old Demon King", 0x400002D0, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Dragonslayer Armour", 0x400002D1, DS3ItemCategory.BOSS, souls = 15000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Nameless King", 0x400002D2, DS3ItemCategory.BOSS, souls = 16000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Pontiff Sulyvahn", 0x400002D4, DS3ItemCategory.BOSS, souls = 12000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Aldrich", 0x400002D5, DS3ItemCategory.BOSS, souls = 15000, + classification = ItemClassification.progression), + DS3ItemData("Soul of High Lord Wolnir", 0x400002D6, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Rotted Greatwood", 0x400002D7, DS3ItemCategory.BOSS, souls = 3000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Rosaria", 0x400002D8, DS3ItemCategory.BOSS, souls = 5000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Deacons of the Deep", 0x400002D9, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Twin Princes", 0x400002DB, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Yhorm the Giant", 0x400002DC, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Lords", 0x400002DD, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Demon", 0x400002E3, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Stray Demon", 0x400002E7, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + *DS3ItemData("Titanite Shard", 0x400003E8, DS3ItemCategory.UPGRADE).counts([2]), + *DS3ItemData("Large Titanite Shard", 0x400003E9, DS3ItemCategory.UPGRADE).counts([2, 3]), + *DS3ItemData("Titanite Chunk", 0x400003EA, DS3ItemCategory.UPGRADE).counts([2, 6]), + DS3ItemData("Titanite Slab", 0x400003EB, DS3ItemCategory.UPGRADE, + classification = ItemClassification.useful), + *DS3ItemData("Titanite Scale", 0x400003FC, DS3ItemCategory.UPGRADE).counts([2, 3]), + *DS3ItemData("Twinkling Titanite", 0x40000406, DS3ItemCategory.UPGRADE).counts([2, 3]), + DS3ItemData("Heavy Gem", 0x4000044C, DS3ItemCategory.UPGRADE), + DS3ItemData("Sharp Gem", 0x40000456, DS3ItemCategory.UPGRADE), + DS3ItemData("Refined Gem", 0x40000460, DS3ItemCategory.UPGRADE), + DS3ItemData("Crystal Gem", 0x4000046A, DS3ItemCategory.UPGRADE), + DS3ItemData("Simple Gem", 0x40000474, DS3ItemCategory.UPGRADE), + DS3ItemData("Fire Gem", 0x4000047E, DS3ItemCategory.UPGRADE), + DS3ItemData("Chaos Gem", 0x40000488, DS3ItemCategory.UPGRADE), + DS3ItemData("Lightning Gem", 0x40000492, DS3ItemCategory.UPGRADE), + DS3ItemData("Deep Gem", 0x4000049C, DS3ItemCategory.UPGRADE), + DS3ItemData("Dark Gem", 0x400004A6, DS3ItemCategory.UPGRADE), + DS3ItemData("Poison Gem", 0x400004B0, DS3ItemCategory.UPGRADE), + DS3ItemData("Blood Gem", 0x400004BA, DS3ItemCategory.UPGRADE), + DS3ItemData("Raw Gem", 0x400004C4, DS3ItemCategory.UPGRADE), + DS3ItemData("Blessed Gem", 0x400004CE, DS3ItemCategory.UPGRADE), + DS3ItemData("Hollow Gem", 0x400004D8, DS3ItemCategory.UPGRADE), + DS3ItemData("Shriving Stone", 0x400004E2, DS3ItemCategory.UPGRADE), + DS3ItemData("Lift Chamber Key", 0x400007D1, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Doll", 0x400007D5, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Jailbreaker's Key", 0x400007D7, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Jailer's Key Ring", 0x400007D8, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Key", 0x400007D9, DS3ItemCategory.UNIQUE), + DS3ItemData("Cell Key", 0x400007DA, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Dungeon Ground Floor Key", 0x400007DB, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Cell Key", 0x400007DC, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grand Archives Key", 0x400007DE, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Tower Key", 0x400007DF, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Lothric Banner", 0x40000836, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Farron Coal", 0x40000837, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Sage's Coal", 0x40000838, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Giant's Coal", 0x40000839, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Profaned Coal", 0x4000083A, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Mortician's Ashes", 0x4000083B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Dreamchaser's Ashes", 0x4000083C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Paladin's Ashes", 0x4000083D, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Warden's Ashes", 0x4000083E, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Greirat's Ashes", 0x4000083F, DS3ItemCategory.UNIQUE), + DS3ItemData("Orbeck's Ashes", 0x40000840, DS3ItemCategory.UNIQUE), + DS3ItemData("Cornyx's Ashes", 0x40000841, DS3ItemCategory.UNIQUE), + DS3ItemData("Karla's Ashes", 0x40000842, DS3ItemCategory.UNIQUE), + DS3ItemData("Irina's Ashes", 0x40000843, DS3ItemCategory.UNIQUE), + DS3ItemData("Yuria's Ashes", 0x40000844, DS3ItemCategory.UNIQUE), + DS3ItemData("Basin of Vows", 0x40000845, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Loretta's Bone", 0x40000846, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Braille Divine Tome of Carim", 0x40000847, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Braille Divine Tome of Lothric", 0x40000848, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Abyss Watcher", 0x4000084B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Aldrich", 0x4000084C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Yhorm the Giant", 0x4000084D, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Lothric Prince", 0x4000084E, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Great Swamp Pyromancy Tome", 0x4000084F, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Carthus Pyromancy Tome", 0x40000850, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Izalith Pyromancy Tome", 0x40000851, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Quelana Pyromancy Tome", 0x40000852, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Warden Pyromancy Tome", 0x40000853, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Sage's Scroll", 0x40000854, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Logan's Scroll", 0x40000855, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Crystal Scroll", 0x40000856, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Transposing Kiln", 0x40000857, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Coiled Sword", 0x40000859, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Eyes of a Fire Keeper", 0x4000085A, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), # Allow players to do any ending + DS3ItemData("Sword of Avowal", 0x4000085B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Golden Scroll", 0x4000085C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Estus Shard", 0x4000085D, DS3ItemCategory.HEALING, + classification = ItemClassification.useful), + DS3ItemData("Hawkwood's Swordgrass", 0x4000085E, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Undead Bone Shard", 0x4000085F, DS3ItemCategory.HEALING, + classification = ItemClassification.useful), + DS3ItemData("Deep Braille Divine Tome", 0x40000860, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Londor Braille Divine Tome", 0x40000861, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Excrement-covered Ashes", 0x40000862, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Prisoner Chief's Ashes", 0x40000863, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Xanthous Ashes", 0x40000864, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Hollow's Ashes", 0x40000865, DS3ItemCategory.UNIQUE), + DS3ItemData("Patches' Ashes", 0x40000866, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon Chaser's Ashes", 0x40000867, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Easterner's Ashes", 0x40000868, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + + # Fake item for controlling access to Archdragon Peak. The real drop isn't actually an item as + # such, so we have to inject this because there's no slot for it to come from. + DS3ItemData("Path of the Dragon", 0x40002346, DS3ItemCategory.UNIQUE, + inject = True, classification = ItemClassification.progression), # Spells - ("Farron Dart", 0x40124F80, DS3ItemCategory.SPELL), - ("Great Farron Dart", 0x40127690, DS3ItemCategory.SPELL), - ("Soul Arrow", 0x4013D620, DS3ItemCategory.SPELL), - ("Great Soul Arrow", 0x4013DA08, DS3ItemCategory.SPELL), - ("Heavy Soul Arrow", 0x4013DDF0, DS3ItemCategory.SPELL), - ("Great Heavy Soul Arrow", 0x4013E1D8, DS3ItemCategory.SPELL), - ("Homing Soulmass", 0x4013E5C0, DS3ItemCategory.SPELL), - ("Homing Crystal Soulmass", 0x4013E9A8, DS3ItemCategory.SPELL), - ("Soul Spear", 0x4013ED90, DS3ItemCategory.SPELL), - ("Crystal Soul Spear", 0x4013F178, DS3ItemCategory.SPELL), - ("Deep Soul", 0x4013F560, DS3ItemCategory.SPELL), - ("Great Deep Soul", 0x4013F948, DS3ItemCategory.SPELL), - ("Magic Weapon", 0x4013FD30, DS3ItemCategory.SPELL), - ("Great Magic Weapon", 0x40140118, DS3ItemCategory.SPELL), - ("Crystal Magic Weapon", 0x40140500, DS3ItemCategory.SPELL), - ("Magic Shield", 0x40144B50, DS3ItemCategory.SPELL), - ("Great Magic Shield", 0x40144F38, DS3ItemCategory.SPELL), - ("Hidden Weapon", 0x40147260, DS3ItemCategory.SPELL), - ("Hidden Body", 0x40147648, DS3ItemCategory.SPELL), - ("Cast Light", 0x40149970, DS3ItemCategory.SPELL), - ("Repair", 0x4014A528, DS3ItemCategory.SPELL), - ("Spook", 0x4014A910, DS3ItemCategory.SPELL), - ("Chameleon", 0x4014ACF8, DS3ItemCategory.SPELL), - ("Aural Decoy", 0x4014B0E0, DS3ItemCategory.SPELL), - ("White Dragon Breath", 0x4014E790, DS3ItemCategory.SPELL), - ("Farron Hail", 0x4014EF60, DS3ItemCategory.SPELL), - ("Crystal Hail", 0x4014F348, DS3ItemCategory.SPELL), - ("Soul Greatsword", 0x4014F730, DS3ItemCategory.SPELL), - ("Farron Flashsword", 0x4014FB18, DS3ItemCategory.SPELL), - ("Affinity", 0x401875B8, DS3ItemCategory.SPELL), - ("Dark Edge", 0x40189CC8, DS3ItemCategory.SPELL), - ("Soul Stream", 0x4018B820, DS3ItemCategory.SPELL), - ("Twisted Wall of Light", 0x40193138, DS3ItemCategory.SPELL), - ("Pestilent Mist", 0x401A8CE0, DS3ItemCategory.SPELL), # Originally called "Pestilent Mercury" pre 1.15 - ("Fireball", 0x40249F00, DS3ItemCategory.SPELL), - ("Fire Orb", 0x4024A6D0, DS3ItemCategory.SPELL), - ("Firestorm", 0x4024AAB8, DS3ItemCategory.SPELL), - ("Fire Surge", 0x4024B288, DS3ItemCategory.SPELL), - ("Black Serpent", 0x4024BA58, DS3ItemCategory.SPELL), - ("Combustion", 0x4024C610, DS3ItemCategory.SPELL), - ("Great Combustion", 0x4024C9F8, DS3ItemCategory.SPELL), - ("Poison Mist", 0x4024ED20, DS3ItemCategory.SPELL), - ("Toxic Mist", 0x4024F108, DS3ItemCategory.SPELL), - ("Acid Surge", 0x4024F4F0, DS3ItemCategory.SPELL), - ("Iron Flesh", 0x40251430, DS3ItemCategory.SPELL), - ("Flash Sweat", 0x40251818, DS3ItemCategory.SPELL), - ("Carthus Flame Arc", 0x402527B8, DS3ItemCategory.SPELL), - ("Rapport", 0x40252BA0, DS3ItemCategory.SPELL), - ("Power Within", 0x40253B40, DS3ItemCategory.SPELL), - ("Great Chaos Fire Orb", 0x40256250, DS3ItemCategory.SPELL), - ("Chaos Storm", 0x40256638, DS3ItemCategory.SPELL), - ("Fire Whip", 0x40256A20, DS3ItemCategory.SPELL), - ("Black Flame", 0x40256E08, DS3ItemCategory.SPELL), - ("Profaned Flame", 0x402575D8, DS3ItemCategory.SPELL), - ("Chaos Bed Vestiges", 0x402579C0, DS3ItemCategory.SPELL), - ("Warmth", 0x4025B070, DS3ItemCategory.SPELL), - ("Profuse Sweat", 0x402717D0, DS3ItemCategory.SPELL), - ("Black Fire Orb", 0x4027D350, DS3ItemCategory.SPELL), - ("Bursting Fireball", 0x4027FA60, DS3ItemCategory.SPELL), - ("Boulder Heave", 0x40282170, DS3ItemCategory.SPELL), - ("Sacred Flame", 0x40284880, DS3ItemCategory.SPELL), - ("Carthus Beacon", 0x40286F90, DS3ItemCategory.SPELL), - ("Heal Aid", 0x403540D0, DS3ItemCategory.SPELL), - ("Heal", 0x403567E0, DS3ItemCategory.SPELL), - ("Med Heal", 0x40356BC8, DS3ItemCategory.SPELL), - ("Great Heal", 0x40356FB0, DS3ItemCategory.SPELL), - ("Soothing Sunlight", 0x40357398, DS3ItemCategory.SPELL), - ("Replenishment", 0x40357780, DS3ItemCategory.SPELL), - ("Bountiful Sunlight", 0x40357B68, DS3ItemCategory.SPELL), - ("Bountiful Light", 0x40358338, DS3ItemCategory.SPELL), - ("Caressing Tears", 0x40358720, DS3ItemCategory.SPELL), - ("Tears of Denial", 0x4035B600, DS3ItemCategory.SPELL), - ("Homeward", 0x4035B9E8, DS3ItemCategory.SPELL), - ("Force", 0x4035DD10, DS3ItemCategory.SPELL), - ("Wrath of the Gods", 0x4035E0F8, DS3ItemCategory.SPELL), - ("Emit Force", 0x4035E4E0, DS3ItemCategory.SPELL), - ("Seek Guidance", 0x40360420, DS3ItemCategory.SPELL), - ("Lightning Spear", 0x40362B30, DS3ItemCategory.SPELL), - ("Great Lightning Spear", 0x40362F18, DS3ItemCategory.SPELL), - ("Sunlight Spear", 0x40363300, DS3ItemCategory.SPELL), - ("Lightning Storm", 0x403636E8, DS3ItemCategory.SPELL), - ("Gnaw", 0x40363AD0, DS3ItemCategory.SPELL), - ("Dorhys' Gnawing", 0x40363EB8, DS3ItemCategory.SPELL), - ("Magic Barrier", 0x40365240, DS3ItemCategory.SPELL), - ("Great Magic Barrier", 0x40365628, DS3ItemCategory.SPELL), - ("Sacred Oath", 0x40365DF8, DS3ItemCategory.SPELL), - ("Vow of Silence", 0x4036A448, DS3ItemCategory.SPELL), - ("Lightning Blade", 0x4036C770, DS3ItemCategory.SPELL), - ("Darkmoon Blade", 0x4036CB58, DS3ItemCategory.SPELL), - ("Dark Blade", 0x40378AC0, DS3ItemCategory.SPELL), - ("Dead Again", 0x40387520, DS3ItemCategory.SPELL), - ("Lightning Stake", 0x40389C30, DS3ItemCategory.SPELL), - ("Divine Pillars of Light", 0x4038C340, DS3ItemCategory.SPELL), - ("Lifehunt Scythe", 0x4038EA50, DS3ItemCategory.SPELL), - ("Blessed Weapon", 0x40395F80, DS3ItemCategory.SPELL), - ("Deep Protection", 0x40398690, DS3ItemCategory.SPELL), - ("Atonement", 0x4039ADA0, DS3ItemCategory.SPELL), -]] - -_dlc_items = [DS3ItemData(row[0], row[1], True, row[2]) for row in [ + DS3ItemData("Farron Dart", 0x40124F80, DS3ItemCategory.SPELL), + DS3ItemData("Great Farron Dart", 0x40127690, DS3ItemCategory.SPELL), + DS3ItemData("Soul Arrow", 0x4013D620, DS3ItemCategory.SPELL), + DS3ItemData("Great Soul Arrow", 0x4013DA08, DS3ItemCategory.SPELL), + DS3ItemData("Heavy Soul Arrow", 0x4013DDF0, DS3ItemCategory.SPELL), + DS3ItemData("Great Heavy Soul Arrow", 0x4013E1D8, DS3ItemCategory.SPELL), + DS3ItemData("Homing Soulmass", 0x4013E5C0, DS3ItemCategory.SPELL), + DS3ItemData("Homing Crystal Soulmass", 0x4013E9A8, DS3ItemCategory.SPELL), + DS3ItemData("Soul Spear", 0x4013ED90, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Soul Spear", 0x4013F178, DS3ItemCategory.SPELL), + DS3ItemData("Deep Soul", 0x4013F560, DS3ItemCategory.SPELL), + DS3ItemData("Great Deep Soul", 0x4013F948, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Magic Weapon", 0x4013FD30, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Weapon", 0x40140118, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Magic Weapon", 0x40140500, DS3ItemCategory.SPELL), + DS3ItemData("Magic Shield", 0x40144B50, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Shield", 0x40144F38, DS3ItemCategory.SPELL), + DS3ItemData("Hidden Weapon", 0x40147260, DS3ItemCategory.SPELL), + DS3ItemData("Hidden Body", 0x40147648, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Cast Light", 0x40149970, DS3ItemCategory.SPELL), + DS3ItemData("Repair", 0x4014A528, DS3ItemCategory.SPELL), + DS3ItemData("Spook", 0x4014A910, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Chameleon", 0x4014ACF8, DS3ItemCategory.SPELL, + classification = ItemClassification.progression), + DS3ItemData("Aural Decoy", 0x4014B0E0, DS3ItemCategory.SPELL), + DS3ItemData("White Dragon Breath", 0x4014E790, DS3ItemCategory.SPELL), + DS3ItemData("Farron Hail", 0x4014EF60, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Hail", 0x4014F348, DS3ItemCategory.SPELL), + DS3ItemData("Soul Greatsword", 0x4014F730, DS3ItemCategory.SPELL), + DS3ItemData("Farron Flashsword", 0x4014FB18, DS3ItemCategory.SPELL), + DS3ItemData("Affinity", 0x401875B8, DS3ItemCategory.SPELL), + DS3ItemData("Dark Edge", 0x40189CC8, DS3ItemCategory.SPELL), + DS3ItemData("Soul Stream", 0x4018B820, DS3ItemCategory.SPELL), + DS3ItemData("Twisted Wall of Light", 0x40193138, DS3ItemCategory.SPELL), + DS3ItemData("Pestilent Mist", 0x401A8CE0, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), # Originally called "Pestilent Mercury" pre 1.15 + DS3ItemData("Fireball", 0x40249F00, DS3ItemCategory.SPELL), + DS3ItemData("Fire Orb", 0x4024A6D0, DS3ItemCategory.SPELL), + DS3ItemData("Firestorm", 0x4024AAB8, DS3ItemCategory.SPELL), + DS3ItemData("Fire Surge", 0x4024B288, DS3ItemCategory.SPELL), + DS3ItemData("Black Serpent", 0x4024BA58, DS3ItemCategory.SPELL), + DS3ItemData("Combustion", 0x4024C610, DS3ItemCategory.SPELL), + DS3ItemData("Great Combustion", 0x4024C9F8, DS3ItemCategory.SPELL), + DS3ItemData("Poison Mist", 0x4024ED20, DS3ItemCategory.SPELL), + DS3ItemData("Toxic Mist", 0x4024F108, DS3ItemCategory.SPELL), + DS3ItemData("Acid Surge", 0x4024F4F0, DS3ItemCategory.SPELL), + DS3ItemData("Iron Flesh", 0x40251430, DS3ItemCategory.SPELL), + DS3ItemData("Flash Sweat", 0x40251818, DS3ItemCategory.SPELL), + DS3ItemData("Carthus Flame Arc", 0x402527B8, DS3ItemCategory.SPELL), + DS3ItemData("Rapport", 0x40252BA0, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Power Within", 0x40253B40, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Great Chaos Fire Orb", 0x40256250, DS3ItemCategory.SPELL), + DS3ItemData("Chaos Storm", 0x40256638, DS3ItemCategory.SPELL), + DS3ItemData("Fire Whip", 0x40256A20, DS3ItemCategory.SPELL), + DS3ItemData("Black Flame", 0x40256E08, DS3ItemCategory.SPELL), + DS3ItemData("Profaned Flame", 0x402575D8, DS3ItemCategory.SPELL), + DS3ItemData("Chaos Bed Vestiges", 0x402579C0, DS3ItemCategory.SPELL), + DS3ItemData("Warmth", 0x4025B070, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Profuse Sweat", 0x402717D0, DS3ItemCategory.SPELL), + DS3ItemData("Black Fire Orb", 0x4027D350, DS3ItemCategory.SPELL), + DS3ItemData("Bursting Fireball", 0x4027FA60, DS3ItemCategory.SPELL), + DS3ItemData("Boulder Heave", 0x40282170, DS3ItemCategory.SPELL), + DS3ItemData("Sacred Flame", 0x40284880, DS3ItemCategory.SPELL), + DS3ItemData("Carthus Beacon", 0x40286F90, DS3ItemCategory.SPELL), + DS3ItemData("Heal Aid", 0x403540D0, DS3ItemCategory.SPELL), + DS3ItemData("Heal", 0x403567E0, DS3ItemCategory.SPELL), + DS3ItemData("Med Heal", 0x40356BC8, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Great Heal", 0x40356FB0, DS3ItemCategory.SPELL), + DS3ItemData("Soothing Sunlight", 0x40357398, DS3ItemCategory.SPELL), + DS3ItemData("Replenishment", 0x40357780, DS3ItemCategory.SPELL), + DS3ItemData("Bountiful Sunlight", 0x40357B68, DS3ItemCategory.SPELL), + DS3ItemData("Bountiful Light", 0x40358338, DS3ItemCategory.SPELL), + DS3ItemData("Caressing Tears", 0x40358720, DS3ItemCategory.SPELL), + DS3ItemData("Tears of Denial", 0x4035B600, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Homeward", 0x4035B9E8, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Force", 0x4035DD10, DS3ItemCategory.SPELL), + DS3ItemData("Wrath of the Gods", 0x4035E0F8, DS3ItemCategory.SPELL), + DS3ItemData("Emit Force", 0x4035E4E0, DS3ItemCategory.SPELL), + DS3ItemData("Seek Guidance", 0x40360420, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Spear", 0x40362B30, DS3ItemCategory.SPELL), + DS3ItemData("Great Lightning Spear", 0x40362F18, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Sunlight Spear", 0x40363300, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Storm", 0x403636E8, DS3ItemCategory.SPELL), + DS3ItemData("Gnaw", 0x40363AD0, DS3ItemCategory.SPELL), + DS3ItemData("Dorhys' Gnawing", 0x40363EB8, DS3ItemCategory.SPELL), + DS3ItemData("Magic Barrier", 0x40365240, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Barrier", 0x40365628, DS3ItemCategory.SPELL), + DS3ItemData("Sacred Oath", 0x40365DF8, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Vow of Silence", 0x4036A448, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Blade", 0x4036C770, DS3ItemCategory.SPELL), + DS3ItemData("Darkmoon Blade", 0x4036CB58, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Dark Blade", 0x40378AC0, DS3ItemCategory.SPELL), + DS3ItemData("Dead Again", 0x40387520, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Stake", 0x40389C30, DS3ItemCategory.SPELL), + DS3ItemData("Divine Pillars of Light", 0x4038C340, DS3ItemCategory.SPELL), + DS3ItemData("Lifehunt Scythe", 0x4038EA50, DS3ItemCategory.SPELL), + DS3ItemData("Blessed Weapon", 0x40395F80, DS3ItemCategory.SPELL), + DS3ItemData("Deep Protection", 0x40398690, DS3ItemCategory.SPELL), + DS3ItemData("Atonement", 0x4039ADA0, DS3ItemCategory.SPELL), +] + +_dlc_items = [ # Ammunition - ("Millwood Greatarrow", 0x000623E0, DS3ItemCategory.SKIP), + *DS3ItemData("Millwood Greatarrow", 0x000623E0, DS3ItemCategory.MISC).counts([5]), # Weapons - ("Aquamarine Dagger", 0x00116520, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Murky Hand Scythe", 0x00118C30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Onyx Blade", 0x00222E00, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Ringed Knight Straight Sword", 0x00225510, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Gael's Greatsword", 0x00227C20, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Follower Sabre", 0x003EDDC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Demon's Scar", 0x003F04D0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Frayed Blade", 0x004D35A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Herald Curved Greatsword", 0x006159E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Millwood Battle Axe", 0x006D67D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Earth Seeker", 0x006D8EE0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Quakestone Hammer", 0x007ECCF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Ledo's Great Hammer", 0x007EF400, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Follower Javelin", 0x008CD6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ringed Knight Spear", 0x008CFDC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lothric War Banner", 0x008D24D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crucifix of the Mad King", 0x008D4BE0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Splitleaf Greatsword", 0x009B2E90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Friede's Great Scythe", 0x009B55A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crow Talons", 0x00A89C10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rose of Ariandel", 0x00B82C70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pyromancer's Parting Flame", 0x00CC9ED0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Murky Longstaff", 0x00CCC5E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sacred Chime of Filianore", 0x00CCECF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Preacher's Right Arm", 0x00CD1400, DS3ItemCategory.WEAPON_UPGRADE_5), - ("White Birch Bow", 0x00D77440, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Millwood Greatbow", 0x00D85EA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Repeating Crossbow", 0x00D885B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Giant Door Shield", 0x00F5F8C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Valorheart", 0x00F646E0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crow Quills", 0x00F66DF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ringed Knight Paired Greatswords", 0x00F69500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Aquamarine Dagger", 0x00116520, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Murky Hand Scythe", 0x00118C30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Onyx Blade", 0x00222E00, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Ringed Knight Straight Sword", 0x00225510, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Gael's Greatsword", 0x00227C20, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Follower Sabre", 0x003EDDC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Demon's Scar", 0x003F04D0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Frayed Blade", 0x004D35A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Harald Curved Greatsword", 0x006159E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Millwood Battle Axe", 0x006D67D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Earth Seeker", 0x006D8EE0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Quakestone Hammer", 0x007ECCF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Ledo's Great Hammer", 0x007EF400, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Follower Javelin", 0x008CD6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ringed Knight Spear", 0x008CFDC0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lothric War Banner", 0x008D24D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crucifix of the Mad King", 0x008D4BE0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Splitleaf Greatsword", 0x009B2E90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Friede's Great Scythe", 0x009B55A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crow Talons", 0x00A89C10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rose of Ariandel", 0x00B82C70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pyromancer's Parting Flame", 0x00CC9ED0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Murky Longstaff", 0x00CCC5E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sacred Chime of Filianore", 0x00CCECF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Preacher's Right Arm", 0x00CD1400, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("White Birch Bow", 0x00D77440, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Millwood Greatbow", 0x00D85EA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Repeating Crossbow", 0x00D885B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Giant Door Shield", 0x00F5F8C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Valorheart", 0x00F646E0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crow Quills", 0x00F66DF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ringed Knight Paired Greatswords", 0x00F69500, DS3ItemCategory.WEAPON_UPGRADE_5), # Shields - ("Follower Shield", 0x0135C0E0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Dragonhead Shield", 0x0135E7F0, DS3ItemCategory.SHIELD), - ("Ethereal Oak Shield", 0x01450320, DS3ItemCategory.SHIELD), - ("Dragonhead Greatshield", 0x01452A30, DS3ItemCategory.SHIELD), - ("Follower Torch", 0x015F1AD0, DS3ItemCategory.SHIELD), + DS3ItemData("Follower Shield", 0x0135C0E0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Dragonhead Shield", 0x0135E7F0, DS3ItemCategory.SHIELD), + DS3ItemData("Ethereal Oak Shield", 0x01450320, DS3ItemCategory.SHIELD), + DS3ItemData("Dragonhead Greatshield", 0x01452A30, DS3ItemCategory.SHIELD), + DS3ItemData("Follower Torch", 0x015F1AD0, DS3ItemCategory.SHIELD), # Armor - ("Vilhelm's Helm", 0x11312D00, DS3ItemCategory.ARMOR), - ("Vilhelm's Armor", 0x113130E8, DS3ItemCategory.ARMOR), - ("Vilhelm's Gauntlets", 0x113134D0, DS3ItemCategory.ARMOR), - ("Vilhelm's Leggings", 0x113138B8, DS3ItemCategory.ARMOR), - ("Antiquated Plain Garb", 0x11B2E408, DS3ItemCategory.ARMOR), - ("Violet Wrappings", 0x11B2E7F0, DS3ItemCategory.ARMOR), - ("Loincloth 2", 0x11B2EBD8, DS3ItemCategory.ARMOR), - ("Shira's Crown", 0x11C22260, DS3ItemCategory.ARMOR), - ("Shira's Armor", 0x11C22648, DS3ItemCategory.ARMOR), - ("Shira's Gloves", 0x11C22A30, DS3ItemCategory.ARMOR), - ("Shira's Trousers", 0x11C22E18, DS3ItemCategory.ARMOR), - ("Lapp's Helm", 0x11E84800, DS3ItemCategory.ARMOR), - ("Lapp's Armor", 0x11E84BE8, DS3ItemCategory.ARMOR), - ("Lapp's Gauntlets", 0x11E84FD0, DS3ItemCategory.ARMOR), - ("Lapp's Leggings", 0x11E853B8, DS3ItemCategory.ARMOR), - ("Slave Knight Hood", 0x134EDCE0, DS3ItemCategory.ARMOR), - ("Slave Knight Armor", 0x134EE0C8, DS3ItemCategory.ARMOR), - ("Slave Knight Gauntlets", 0x134EE4B0, DS3ItemCategory.ARMOR), - ("Slave Knight Leggings", 0x134EE898, DS3ItemCategory.ARMOR), - ("Ordained Hood", 0x135E1F20, DS3ItemCategory.ARMOR), - ("Ordained Dress", 0x135E2308, DS3ItemCategory.ARMOR), - ("Ordained Trousers", 0x135E2AD8, DS3ItemCategory.ARMOR), - ("Follower Helm", 0x137CA3A0, DS3ItemCategory.ARMOR), - ("Follower Armor", 0x137CA788, DS3ItemCategory.ARMOR), - ("Follower Gloves", 0x137CAB70, DS3ItemCategory.ARMOR), - ("Follower Boots", 0x137CAF58, DS3ItemCategory.ARMOR), - ("Millwood Knight Helm", 0x139B2820, DS3ItemCategory.ARMOR), - ("Millwood Knight Armor", 0x139B2C08, DS3ItemCategory.ARMOR), - ("Millwood Knight Gauntlets", 0x139B2FF0, DS3ItemCategory.ARMOR), - ("Millwood Knight Leggings", 0x139B33D8, DS3ItemCategory.ARMOR), - ("Ringed Knight Hood", 0x13C8EEE0, DS3ItemCategory.ARMOR), - ("Ringed Knight Armor", 0x13C8F2C8, DS3ItemCategory.ARMOR), - ("Ringed Knight Gauntlets", 0x13C8F6B0, DS3ItemCategory.ARMOR), - ("Ringed Knight Leggings", 0x13C8FA98, DS3ItemCategory.ARMOR), - ("Harald Legion Armor", 0x13D83508, DS3ItemCategory.ARMOR), - ("Harald Legion Gauntlets", 0x13D838F0, DS3ItemCategory.ARMOR), - ("Harald Legion Leggings", 0x13D83CD8, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Helm", 0x1405F7E0, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Armor", 0x1405FBC8, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Gauntlets", 0x1405FFB0, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Leggings", 0x14060398, DS3ItemCategory.ARMOR), - ("White Preacher Head", 0x14153A20, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Helm", 0x14CC5520, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Armor", 0x14CC5908, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Gauntlets", 0x14CC5CF0, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Leggings", 0x14CC60D8, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Hood", 0x14DB9760, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Garb", 0x14DB9B48, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Gloves", 0x14DB9F30, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Skirt", 0x14DBA318, DS3ItemCategory.ARMOR), - ("Black Witch Hat", 0x14EAD9A0, DS3ItemCategory.ARMOR), - ("Black Witch Garb", 0x14EADD88, DS3ItemCategory.ARMOR), - ("Black Witch Wrappings", 0x14EAE170, DS3ItemCategory.ARMOR), - ("Black Witch Trousers", 0x14EAE558, DS3ItemCategory.ARMOR), - ("Black Witch Veil", 0x14FA1BE0, DS3ItemCategory.ARMOR), - ("Blindfold Mask", 0x15095E20, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Helm", 0x11312D00, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Armor", 0x113130E8, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Gauntlets", 0x113134D0, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Leggings", 0x113138B8, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Plain Garb", 0x11B2E408, DS3ItemCategory.ARMOR), + DS3ItemData("Violet Wrappings", 0x11B2E7F0, DS3ItemCategory.ARMOR), + DS3ItemData("Loincloth 2", 0x11B2EBD8, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Crown", 0x11C22260, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Armor", 0x11C22648, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Gloves", 0x11C22A30, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Trousers", 0x11C22E18, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Helm", 0x11E84800, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Armor", 0x11E84BE8, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Gauntlets", 0x11E84FD0, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Leggings", 0x11E853B8, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Hood", 0x134EDCE0, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Armor", 0x134EE0C8, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Gauntlets", 0x134EE4B0, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Leggings", 0x134EE898, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Hood", 0x135E1F20, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Dress", 0x135E2308, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Trousers", 0x135E2AD8, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Helm", 0x137CA3A0, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Armor", 0x137CA788, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Gloves", 0x137CAB70, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Boots", 0x137CAF58, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Helm", 0x139B2820, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Armor", 0x139B2C08, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Gauntlets", 0x139B2FF0, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Leggings", 0x139B33D8, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Hood", 0x13C8EEE0, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Armor", 0x13C8F2C8, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Gauntlets", 0x13C8F6B0, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Leggings", 0x13C8FA98, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Armor", 0x13D83508, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Gauntlets", 0x13D838F0, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Leggings", 0x13D83CD8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Helm", 0x1405F7E0, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Armor", 0x1405FBC8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Gauntlets", 0x1405FFB0, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Leggings", 0x14060398, DS3ItemCategory.ARMOR), + DS3ItemData("White Preacher Head", 0x14153A20, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Helm", 0x14CC5520, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Armor", 0x14CC5908, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Gauntlets", 0x14CC5CF0, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Leggings", 0x14CC60D8, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Hood", 0x14DB9760, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Garb", 0x14DB9B48, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Gloves", 0x14DB9F30, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Skirt", 0x14DBA318, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Hat", 0x14EAD9A0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Garb", 0x14EADD88, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Wrappings", 0x14EAE170, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Trousers", 0x14EAE558, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Veil", 0x14FA1BE0, DS3ItemCategory.ARMOR), + DS3ItemData("Blindfold Mask", 0x15095E20, DS3ItemCategory.ARMOR), # Covenants - ("Spear of the Church", 0x2000276A, DS3ItemCategory.SKIP), + DS3ItemData("Spear of the Church", 0x2000276A, DS3ItemCategory.UNIQUE, skip = True), # Rings - ("Chloranthy Ring+3", 0x20004E2D, DS3ItemCategory.RING), - ("Havel's Ring+3", 0x20004E37, DS3ItemCategory.RING), - ("Ring of Favor+3", 0x20004E41, DS3ItemCategory.RING), - ("Ring of Steel Protection+3", 0x20004E4B, DS3ItemCategory.RING), - ("Wolf Ring+3", 0x20004EE1, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+3", 0x20004FA9, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+3", 0x20004FB3, DS3ItemCategory.RING), - ("Ring of the Evil Eye+3", 0x20005071, DS3ItemCategory.RING), - ("Chillbite Ring", 0x20005208, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring+3", 0x20004E2D, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Havel's Ring+3", 0x20004E37, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of Favor+3", 0x20004E41, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of Steel Protection+3", 0x20004E4B, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Wolf Ring+3", 0x20004EE1, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+3", 0x20004FA9, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring+3", 0x20004FB3, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of the Evil Eye+3", 0x20005071, DS3ItemCategory.RING), + DS3ItemData("Chillbite Ring", 0x20005208, DS3ItemCategory.RING), # Items - ("Church Guardian Shiv", 0x4000013B, DS3ItemCategory.MISC), - ("Filianore's Spear Ornament", 0x4000017B, DS3ItemCategory.SKIP), - ("Ritual Spear Fragment", 0x4000028A, DS3ItemCategory.MISC), - ("Divine Spear Fragment", 0x4000028B, DS3ItemCategory.SKIP), - ("Soul of Sister Friede", 0x400002E8, DS3ItemCategory.BOSS), - ("Soul of Slave Knight Gael", 0x400002E9, DS3ItemCategory.BOSS), - ("Soul of the Demon Prince", 0x400002EA, DS3ItemCategory.BOSS), - ("Soul of Darkeater Midir", 0x400002EB, DS3ItemCategory.BOSS), - ("Champion's Bones", 0x40000869, DS3ItemCategory.SKIP), - ("Captain's Ashes", 0x4000086A, DS3ItemCategory.MISC), - ("Contraption Key", 0x4000086B, DS3ItemCategory.KEY), - ("Small Envoy Banner", 0x4000086C, DS3ItemCategory.KEY), - ("Old Woman's Ashes", 0x4000086D, DS3ItemCategory.SKIP), - ("Blood of the Dark Soul", 0x4000086E, DS3ItemCategory.SKIP), + DS3ItemData("Church Guardian Shiv", 0x4000013B, DS3ItemCategory.MISC), + DS3ItemData("Filianore's Spear Ornament", 0x4000017B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Ritual Spear Fragment", 0x4000028A, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Divine Spear Fragment", 0x4000028B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Soul of Sister Friede", 0x400002E8, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Slave Knight Gael", 0x400002E9, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Demon Prince", 0x400002EA, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Darkeater Midir", 0x400002EB, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Champion's Bones", 0x40000869, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Captain's Ashes", 0x4000086A, DS3ItemCategory.MISC, + classification = ItemClassification.progression), + DS3ItemData("Contraption Key", 0x4000086B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Envoy Banner", 0x4000086C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Old Woman's Ashes", 0x4000086D, DS3ItemCategory.UNIQUE), + DS3ItemData("Blood of the Dark Soul", 0x4000086E, DS3ItemCategory.UNIQUE, skip = True), # Spells - ("Frozen Weapon", 0x401408E8, DS3ItemCategory.SPELL), - ("Old Moonlight", 0x4014FF00, DS3ItemCategory.SPELL), - ("Great Soul Dregs", 0x401879A0, DS3ItemCategory.SPELL), - ("Snap Freeze", 0x401A90C8, DS3ItemCategory.SPELL), - ("Floating Chaos", 0x40257DA8, DS3ItemCategory.SPELL), - ("Flame Fan", 0x40258190, DS3ItemCategory.SPELL), - ("Seething Chaos", 0x402896A0, DS3ItemCategory.SPELL), - ("Lightning Arrow", 0x40358B08, DS3ItemCategory.SPELL), - ("Way of White Corona", 0x403642A0, DS3ItemCategory.SPELL), - ("Projected Heal", 0x40364688, DS3ItemCategory.SPELL), -]] + DS3ItemData("Frozen Weapon", 0x401408E8, DS3ItemCategory.SPELL), + DS3ItemData("Old Moonlight", 0x4014FF00, DS3ItemCategory.SPELL), + DS3ItemData("Great Soul Dregs", 0x401879A0, DS3ItemCategory.SPELL), + DS3ItemData("Snap Freeze", 0x401A90C8, DS3ItemCategory.SPELL), + DS3ItemData("Floating Chaos", 0x40257DA8, DS3ItemCategory.SPELL), + DS3ItemData("Flame Fan", 0x40258190, DS3ItemCategory.SPELL), + DS3ItemData("Seething Chaos", 0x402896A0, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Arrow", 0x40358B08, DS3ItemCategory.SPELL), + DS3ItemData("Way of White Corona", 0x403642A0, DS3ItemCategory.SPELL), + DS3ItemData("Projected Heal", 0x40364688, DS3ItemCategory.SPELL), +] +for item in _dlc_items: + item.is_dlc = True # Unused list for future reference # These items exist to some degree in the code, but aren't accessible # in-game and can't be picked up without modifications -_cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ +_cut_content_items = [ # Weapons - ("Blood-stained Short Sword", 0x00100590, DS3ItemCategory.SKIP), - ("Missionary's Axe", 0x006C2F50, DS3ItemCategory.SKIP), - ("Dragon King Greataxe", 0x006D40C0, DS3ItemCategory.SKIP), - ("Four Knights Hammer", 0x007D4650, DS3ItemCategory.SKIP), - ("Hammer of the Great Tree", 0x007D9470, DS3ItemCategory.SKIP), - ("Lothric's Scythe", 0x009A4430, DS3ItemCategory.SKIP), - ("Ancient Dragon Halberd", 0x009A6B40, DS3ItemCategory.SKIP), - ("Scythe of Want", 0x009A9250, DS3ItemCategory.SKIP), - ("Sacred Beast Catalyst", 0x00C8A730, DS3ItemCategory.SKIP), - ("Deep Pyromancy Flame", 0x00CC9ED0, DS3ItemCategory.SKIP), # Duplicate? - ("Flickering Pyromancy Flame", 0x00CD3B10, DS3ItemCategory.SKIP), - ("Strong Pyromancy Flame", 0x00CD6220, DS3ItemCategory.SKIP), - ("Deep Pyromancy Flame", 0x00CDFE60, DS3ItemCategory.SKIP), # Duplicate? - ("Pitch-Dark Pyromancy Flame", 0x00CE2570, DS3ItemCategory.SKIP), - ("Dancer's Short Bow", 0x00D77440, DS3ItemCategory.SKIP), - ("Shield Crossbow", 0x00D81080, DS3ItemCategory.SKIP), - ("Golden Dual Swords", 0x00F55C80, DS3ItemCategory.SKIP), - ("Channeler's Trident", 0x008C8890, DS3ItemCategory.SKIP), + DS3ItemData("Blood-stained Short Sword", 0x00100590, DS3ItemCategory.UNIQUE), + DS3ItemData("Missionary's Axe", 0x006C2F50, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon King Greataxe", 0x006D40C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Four Knights Hammer", 0x007D4650, DS3ItemCategory.UNIQUE), + DS3ItemData("Hammer of the Great Tree", 0x007D9470, DS3ItemCategory.UNIQUE), + DS3ItemData("Lothric's Scythe", 0x009A4430, DS3ItemCategory.UNIQUE), + DS3ItemData("Ancient Dragon Halberd", 0x009A6B40, DS3ItemCategory.UNIQUE), + DS3ItemData("Scythe of Want", 0x009A9250, DS3ItemCategory.UNIQUE), + DS3ItemData("Sacred Beast Catalyst", 0x00C8A730, DS3ItemCategory.UNIQUE), + DS3ItemData("Deep Pyromancy Flame", 0x00CC9ED0, DS3ItemCategory.UNIQUE), # Duplicate? + DS3ItemData("Flickering Pyromancy Flame", 0x00CD3B10, DS3ItemCategory.UNIQUE), + DS3ItemData("Strong Pyromancy Flame", 0x00CD6220, DS3ItemCategory.UNIQUE), + DS3ItemData("Deep Pyromancy Flame", 0x00CDFE60, DS3ItemCategory.UNIQUE), # Duplicate? + DS3ItemData("Pitch-Dark Pyromancy Flame", 0x00CE2570, DS3ItemCategory.UNIQUE), + DS3ItemData("Dancer's Short Bow", 0x00D77440, DS3ItemCategory.UNIQUE), + DS3ItemData("Shield Crossbow", 0x00D81080, DS3ItemCategory.UNIQUE), + DS3ItemData("Golden Dual Swords", 0x00F55C80, DS3ItemCategory.UNIQUE), + DS3ItemData("Channeler's Trident", 0x008C8890, DS3ItemCategory.UNIQUE), # Shields - ("Cleric's Parma", 0x013524A0, DS3ItemCategory.SKIP), - ("Prince's Shield", 0x01421CF0, DS3ItemCategory.SKIP), + DS3ItemData("Cleric's Parma", 0x013524A0, DS3ItemCategory.UNIQUE), + DS3ItemData("Prince's Shield", 0x01421CF0, DS3ItemCategory.UNIQUE), # Armor - ("Dingy Maiden's Overcoat", 0x11DA9048, DS3ItemCategory.SKIP), - ("Grotto Hat", 0x11F78A40, DS3ItemCategory.SKIP), - ("Grotto Robe", 0x11F78E28, DS3ItemCategory.SKIP), - ("Grotto Wrap", 0x11F79210, DS3ItemCategory.SKIP), - ("Grotto Trousers", 0x11F795F8, DS3ItemCategory.SKIP), - ("Soldier's Gauntlets", 0x126261D0, DS3ItemCategory.SKIP), - ("Soldier's Hood", 0x1263E0A0, DS3ItemCategory.SKIP), - ("Elder's Robe", 0x129024A8, DS3ItemCategory.SKIP), - ("Saint's Veil", 0x12A70420, DS3ItemCategory.SKIP), - ("Saint's Dress", 0x12A70808, DS3ItemCategory.SKIP), - ("Footman's Hood", 0x12AEA540, DS3ItemCategory.SKIP), - ("Footman's Overcoat", 0x12AEA928, DS3ItemCategory.SKIP), - ("Footman's Bracelets", 0x12AEAD10, DS3ItemCategory.SKIP), - ("Footman's Trousers", 0x12AEB0F8, DS3ItemCategory.SKIP), - ("Scholar's Shed Skin", 0x12E40D20, DS3ItemCategory.SKIP), - ("Man Serpent's Mask", 0x138BE5E0, DS3ItemCategory.SKIP), - ("Man Serpent's Robe", 0x138BE9C8, DS3ItemCategory.SKIP), - ("Old Monarch's Crown", 0x13DFD240, DS3ItemCategory.SKIP), - ("Old Monarch's Robe", 0x13DFD628, DS3ItemCategory.SKIP), - ("Frigid Valley Mask", 0x13FE56C0, DS3ItemCategory.SKIP), - ("Dingy Hood", 0x140D9900, DS3ItemCategory.SKIP), - ("Hexer's Hood", 0x15A995C0, DS3ItemCategory.SKIP), - ("Hexer's Robes", 0x15A999A8, DS3ItemCategory.SKIP), - ("Hexer's Gloves", 0x15A99D90, DS3ItemCategory.SKIP), - ("Hexer's Boots", 0x15A9A178, DS3ItemCategory.SKIP), - ("Varangian Helm", 0x15C81A40, DS3ItemCategory.SKIP), - ("Varangian Armor", 0x15C81E28, DS3ItemCategory.SKIP), - ("Varangian Cuffs", 0x15C82210, DS3ItemCategory.SKIP), - ("Varangian Leggings", 0x15C825F8, DS3ItemCategory.SKIP), + DS3ItemData("Dingy Maiden's Overcoat", 0x11DA9048, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Hat", 0x11F78A40, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Robe", 0x11F78E28, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Wrap", 0x11F79210, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Trousers", 0x11F795F8, DS3ItemCategory.UNIQUE), + DS3ItemData("Soldier's Gauntlets", 0x126261D0, DS3ItemCategory.UNIQUE), + DS3ItemData("Soldier's Hood", 0x1263E0A0, DS3ItemCategory.UNIQUE), + DS3ItemData("Elder's Robe", 0x129024A8, DS3ItemCategory.UNIQUE), + DS3ItemData("Saint's Veil", 0x12A70420, DS3ItemCategory.UNIQUE), + DS3ItemData("Saint's Dress", 0x12A70808, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Hood", 0x12AEA540, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Overcoat", 0x12AEA928, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Bracelets", 0x12AEAD10, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Trousers", 0x12AEB0F8, DS3ItemCategory.UNIQUE), + DS3ItemData("Scholar's Shed Skin", 0x12E40D20, DS3ItemCategory.UNIQUE), + DS3ItemData("Man Serpent's Mask", 0x138BE5E0, DS3ItemCategory.UNIQUE), + DS3ItemData("Man Serpent's Robe", 0x138BE9C8, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Monarch's Crown", 0x13DFD240, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Monarch's Robe", 0x13DFD628, DS3ItemCategory.UNIQUE), + DS3ItemData("Frigid Valley Mask", 0x13FE56C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Dingy Hood", 0x140D9900, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Hood", 0x15A995C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Robes", 0x15A999A8, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Gloves", 0x15A99D90, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Boots", 0x15A9A178, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Helm", 0x15C81A40, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Armor", 0x15C81E28, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Cuffs", 0x15C82210, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Leggings", 0x15C825F8, DS3ItemCategory.UNIQUE), # Rings - ("Rare Ring of Sacrifice", 0x20004EFC, DS3ItemCategory.SKIP), - ("Baneful Bird Ring", 0x20005032, DS3ItemCategory.SKIP), - ("Darkmoon Blade Covenant Ring", 0x20004F7E, DS3ItemCategory.SKIP), - ("Yorgh's Ring", 0x2000505A, DS3ItemCategory.SKIP), - ("Ring of Hiding", 0x200050D2, DS3ItemCategory.SKIP), - ("Ring of Sustained Toughness", 0x20005118, DS3ItemCategory.SKIP), - ("Ring of Sustained Energy", 0x20005122, DS3ItemCategory.SKIP), - ("Ring of Sustained Magic", 0x2000512C, DS3ItemCategory.SKIP), - ("Ring of Sustained Essence", 0x20005140, DS3ItemCategory.SKIP), - ("Ring of Sustained Might", 0x2000514A, DS3ItemCategory.SKIP), - ("Ring of Sustained Fortune", 0x20005154, DS3ItemCategory.SKIP), + DS3ItemData("Rare Ring of Sacrifice", 0x20004EFC, DS3ItemCategory.UNIQUE), + DS3ItemData("Baneful Bird Ring", 0x20005032, DS3ItemCategory.UNIQUE), + DS3ItemData("Darkmoon Blade Covenant Ring", 0x20004F7E, DS3ItemCategory.UNIQUE), + DS3ItemData("Yorgh's Ring", 0x2000505A, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Hiding", 0x200050D2, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Toughness", 0x20005118, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Energy", 0x20005122, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Magic", 0x2000512C, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Essence", 0x20005140, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Might", 0x2000514A, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Fortune", 0x20005154, DS3ItemCategory.UNIQUE), # Items - ("Soul of a Wicked Spirit", 0x400002C9, DS3ItemCategory.SKIP), + DS3ItemData("Soul of a Wicked Spirit", 0x400002C9, DS3ItemCategory.UNIQUE), # Spells - ("Dark Orb", 0x4027AC40, DS3ItemCategory.SKIP), - ("Morbid Temptation", 0x40359AA8, DS3ItemCategory.SKIP), - ("Dorris Swarm", 0x40393870, DS3ItemCategory.SKIP), -]] + DS3ItemData("Dark Orb", 0x4027AC40, DS3ItemCategory.UNIQUE), + DS3ItemData("Morbid Temptation", 0x40359AA8, DS3ItemCategory.UNIQUE), + DS3ItemData("Dorris Swarm", 0x40393870, DS3ItemCategory.UNIQUE), +] + + +item_name_groups: Dict[str, Set] = { + "Progression": set(), + "Cinders": set(), + "Weapons": set(), + "Shields": set(), + "Armor": set(), + "Rings": set(), + "Spells": set(), + "Miscellaneous": set(), + "Unique": set(), + "Boss Souls": set(), + "Small Souls": set(), + "Upgrade": set(), + "Healing": set(), +} + item_descriptions = { + "Progression": "Items which unlock locations.", "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", + "Miscellaneous": "Generic stackable items, such as arrows, firebombs, buffs, and so on.", + "Unique": "Items that are unique per NG cycle, such as scrolls, keys, ashes, and so on. Doesn't include equipment, spells, or souls.", + "Boss Souls": "Souls that can be traded with Ludleth, including Soul of Rosaria.", + "Small Souls": "Soul items, not including boss souls.", + "Upgrade": "Upgrade items, including titanite, gems, and Shriving Stones.", + "Healing": "Undead Bone Shards and Estus Shards.", } + _all_items = _vanilla_items + _dlc_items +for item_data in _all_items: + for group_name in item_data.item_groups(): + item_name_groups[group_name].add(item_data.name) + +filler_item_names = [item_data.name for item_data in _all_items if item_data.filler] item_dictionary = {item_data.name: item_data for item_data in _all_items} diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index df241a5fd1fb..08f4b7cd1a80 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -1,693 +1,3119 @@ -from enum import IntEnum -from typing import Optional, NamedTuple, Dict +from typing import cast, ClassVar, Optional, Dict, List, Set +from dataclasses import dataclass -from BaseClasses import Location, Region +from BaseClasses import ItemClassification, Location, Region +from .Items import DS3ItemCategory, item_dictionary +# Regions in approximate order of reward, mostly measured by how high-quality the upgrade items are +# in each region. +region_order = [ + "Cemetery of Ash", + "Firelink Shrine", + "High Wall of Lothric", + "Greirat's Shop", + "Undead Settlement", + "Road of Sacrifices", + "Farron Keep", + "Cathedral of the Deep", + "Catacombs of Carthus", + "Smouldering Lake", + "Irithyll of the Boreal Valley", + "Irithyll Dungeon", + "Karla's Shop", + # The first half of Painted World has one Titanite Slab but mostly Large Titanite Shards, + # much like Irithyll Dungeon. + "Painted World of Ariandel (Before Contraption)", + "Anor Londo", + "Profaned Capital", + # The second half of Painted World has two Titanite Chunks and two Titanite Slabs, which + # puts it on the low end of the post-Lothric Castle areas in terms of rewards. + "Painted World of Ariandel (After Contraption)", + "Lothric Castle", + "Consumed King's Garden", + "Untended Graves", + # List this late because it contains a Titanite Slab in the base game + "Firelink Shrine Bell Tower", + "Grand Archives", + "Archdragon Peak", + "Kiln of the First Flame", + # Both areas of DLC2 have premium rewards. + "Dreg Heap", + "Ringed City", +] -class DS3LocationCategory(IntEnum): - WEAPON = 0 - SHIELD = 1 - ARMOR = 2 - RING = 3 - SPELL = 4 - NPC = 5 - KEY = 6 - BOSS = 7 - MISC = 8 - HEALTH = 9 - PROGRESSIVE_ITEM = 10 - EVENT = 11 +@dataclass +class DS3LocationData: + __location_id: ClassVar[int] = 100000 + """The next location ID to use when creating location data.""" -class DS3LocationData(NamedTuple): name: str - default_item: str - category: DS3LocationCategory + """The name of this location according to Archipelago. + + This needs to be unique within this world.""" + + default_item_name: Optional[str] + """The name of the item that appears by default in this location. + + If this is None, that indicates that this location is an "event" that's + automatically considered accessed as soon as it's available. Events are used + to indicate major game transitions that aren't otherwise gated by items so + that progression balancing and item smoothing is more accurate for DS3. + """ + + ap_code: Optional[int] = None + """Archipelago's internal ID for this location (also known as its "address").""" + + region_value: int = 0 + """The relative value of items in this location's region. + + This is used to sort locations when placing items like the base game. + """ + + static: Optional[str] = None + """The key in the static randomizer's Slots table that corresponds to this location. + + By default, the static randomizer chooses its location based on the region and the item name. + If the item name is unique across the whole game, it can also look it up based on that alone. If + there are multiple instances of the same item type in the same region, it will assume its order + (in annotations.txt) matches Archipelago's order. + + In cases where this heuristic doesn't work, such as when Archipelago's region categorization or + item name disagrees with the static randomizer's, this field is used to provide an explicit + association instead. + """ + + missable: bool = False + """Whether this item is possible to permanently lose access to. + + This is also used for items that are *technically* possible to get at any time, but are + prohibitively difficult without blocking off other checks (items dropped by NPCs on death + generally fall into this category). + + Missable locations are always marked as excluded, so they will never contain + progression or useful items. + """ + + dlc: bool = False + """Whether this location is only accessible if the DLC is enabled.""" + + ngp: bool = False + """Whether this location only contains an item in NG+ and later. + + By default, these items aren't randomized or included in the randomization pool, but an option + can be set to enable them even for NG runs.""" + + npc: bool = False + """Whether this item is contingent on killing an NPC or following their quest.""" + + prominent: bool = False + """Whether this is one of few particularly prominent places for items to appear. + + This is a small number of locations (boss drops and progression locations) + intended to be set as priority locations for players who don't want a lot of + mandatory checks. + + For bosses with multiple drops, only one should be marked prominent. + """ + + progression: bool = False + """Whether this location normally contains an item that blocks forward progress.""" + + boss: bool = False + """Whether this location is a reward for defeating a full boss.""" + + miniboss: bool = False + """Whether this location is a reward for defeating a miniboss. + + The classification of "miniboss" is a bit fuzzy, but we consider them to be enemies that are + visually distinctive in their locations, usually bigger than normal enemies, with a guaranteed + item drop. NPCs are never considered minibosses, and some normal-looking enemies with guaranteed + drops aren't either (these are instead classified as hidden locations).""" + + drop: bool = False + """Whether this is an item dropped by a (non-boss) enemy. + + This is automatically set to True if miniboss, mimic, lizard, or hostile_npc is True. + """ + + mimic: bool = False + """Whether this location is dropped by a mimic.""" + + hostile_npc: bool = False + """Whether this location is dropped by a hostile NPC. + + An "NPC" is specifically a human (or rather, ash) is built like a player character rather than a + monster. This includes both scripted invaders and NPCs who are always on the overworld. It does + not include initially-friendly NPCs who become hostile as part of a quest or because you attack + them. + """ + + lizard: bool = False + """Whether this location is dropped by a (small) Crystal Lizard.""" + + shop: bool = False + """Whether this location can appear in an NPC's shop. + + Items like Lapp's Set which can appear both in the overworld and in a shop + should still be tagged as shop. + """ + + conditional: bool = False + """Whether this location is conditional on a progression item. + + This is used to track locations that won't become available until an unknown amount of time into + the run, and as such shouldn't have "similar to the base game" items placed in them. + """ + + hidden: bool = False + """Whether this location is particularly tricky to find. + + This is for players without an encyclopedic knowledge of DS3 who don't want to get stuck looking + for an illusory wall or one random mob with a guaranteed drop. + """ + + @property + def is_event(self) -> bool: + """Whether this location represents an event rather than a specific item pickup.""" + return self.default_item_name is None + + def __post_init__(self): + if not self.is_event: + self.ap_code = self.ap_code or DS3LocationData.__location_id + DS3LocationData.__location_id += 1 + if self.miniboss or self.mimic or self.lizard or self.hostile_npc: self.drop = True + + def location_groups(self) -> List[str]: + """The names of location groups this location should appear in. + + This is computed from the properties assigned to this location.""" + names = [] + if self.prominent: names.append("Prominent") + if self.progression: names.append("Progression") + if self.boss: names.append("Boss Rewards") + if self.miniboss: names.append("Miniboss Rewards") + if self.mimic: names.append("Mimic Rewards") + if self.hostile_npc: names.append("Hostile NPC Rewards") + if self.npc: names.append("Friendly NPC Rewards") + if self.lizard: names.append("Small Crystal Lizards") + if self.hidden: names.append("Hidden") + + default_item = item_dictionary[cast(str, self.default_item_name)] + names.append({ + DS3ItemCategory.WEAPON_UPGRADE_5: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE: "Weapons", + DS3ItemCategory.SHIELD: "Shields", + DS3ItemCategory.SHIELD_INFUSIBLE: "Shields", + DS3ItemCategory.ARMOR: "Armor", + DS3ItemCategory.RING: "Rings", + DS3ItemCategory.SPELL: "Spells", + DS3ItemCategory.MISC: "Miscellaneous", + DS3ItemCategory.UNIQUE: "Unique", + DS3ItemCategory.BOSS: "Boss Souls", + DS3ItemCategory.SOUL: "Small Souls", + DS3ItemCategory.UPGRADE: "Upgrade", + DS3ItemCategory.HEALING: "Healing", + }[default_item.category]) + if default_item.classification == ItemClassification.progression: + names.append("Progression") + + return names class DarkSouls3Location(Location): game: str = "Dark Souls III" - category: DS3LocationCategory - default_item_name: str + data: DS3LocationData def __init__( self, player: int, - name: str, - category: DS3LocationCategory, - default_item_name: str, - address: Optional[int] = None, - parent: Optional[Region] = None): - super().__init__(player, name, address, parent) - self.default_item_name = default_item_name - self.category = category - - @staticmethod - def get_name_to_id() -> dict: - base_id = 100000 - table_offset = 100 - - table_order = [ - "Firelink Shrine", - "Firelink Shrine Bell Tower", - "High Wall of Lothric", - "Undead Settlement", - "Road of Sacrifices", - "Cathedral of the Deep", - "Farron Keep", - "Catacombs of Carthus", - "Smouldering Lake", - "Irithyll of the Boreal Valley", - "Irithyll Dungeon", - "Profaned Capital", - "Anor Londo", - "Lothric Castle", - "Consumed King's Garden", - "Grand Archives", - "Untended Graves", - "Archdragon Peak", - - "Painted World of Ariandel 1", - "Painted World of Ariandel 2", - "Dreg Heap", - "Ringed City", - - "Progressive Items 1", - "Progressive Items 2", - "Progressive Items 3", - "Progressive Items 4", - "Progressive Items DLC", - "Progressive Items Health", - ] - - output = {} - for i, region_name in enumerate(table_order): - if len(location_tables[region_name]) > table_offset: - raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(location_tables[region_name]), table_offset, i)) - - output.update({location_data.name: id for id, location_data in enumerate(location_tables[region_name], base_id + (table_offset * i))}) - - return output - - -location_tables = { + data: DS3LocationData, + parent: Optional[Region] = None, + event: bool = False): + super().__init__(player, data.name, None if event else data.ap_code, parent) + self.data = data + + +# Naming conventions: +# +# * The regions in item names should match the physical region where the item is +# acquired, even if its logical region is different. For example, Irina's +# inventory appears in the "Undead Settlement" region because she's not +# accessible until there, but it begins with "FS:" because that's where her +# items are purchased. +# +# * Avoid using vanilla enemy placements as landmarks, because these are +# randomized by the enemizer by default. Instead, use generic terms like +# "mob", "boss", and "miniboss". +# +# * Location descriptions don't need to direct the player to the precise spot. +# You can assume the player is broadly familiar with Dark Souls III or willing +# to look at a vanilla guide. Just give a general area to look in or an idea +# of what quest a check is connected to. Terseness is valuable: try to keep +# each location description short enough that the whole line doesn't exceed +# 100 characters. +# +# * Use "[name] drop" for items that require killing an NPC who becomes hostile +# as part of their normal quest, "kill [name]" for items that require killing +# them even when they aren't hostile, and just "[name]" for items that are +# naturally available as part of their quest. +location_tables: Dict[str, List[DS3LocationData]] = { + "Cemetery of Ash": [ + DS3LocationData("CA: Soul of a Deserted Corpse - right of spawn", + "Soul of a Deserted Corpse"), + DS3LocationData("CA: Firebomb - down the cliff edge", "Firebomb x5"), + DS3LocationData("CA: Titanite Shard - jump to coffin", "Titanite Shard"), + DS3LocationData("CA: Soul of an Unknown Traveler - by miniboss", + "Soul of an Unknown Traveler"), + DS3LocationData("CA: Speckled Stoneplate Ring+1 - by miniboss", + "Speckled Stoneplate Ring+1", ngp=True), + DS3LocationData("CA: Titanite Scale - miniboss drop", "Titanite Scale", miniboss=True), + DS3LocationData("CA: Coiled Sword - boss drop", "Coiled Sword", prominent=True, + progression=True, boss=True), + ], "Firelink Shrine": [ - DS3LocationData("FS: Broken Straight Sword", "Broken Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("FS: East-West Shield", "East-West Shield", DS3LocationCategory.SHIELD), - DS3LocationData("FS: Uchigatana", "Uchigatana", DS3LocationCategory.WEAPON), - DS3LocationData("FS: Master's Attire", "Master's Attire", DS3LocationCategory.ARMOR), - DS3LocationData("FS: Master's Gloves", "Master's Gloves", DS3LocationCategory.ARMOR), + # Ludleth drop, does not permanently die + DS3LocationData("FS: Skull Ring - kill Ludleth", "Skull Ring", hidden=True, drop=True, + npc=True), + + # Sword Master drops + DS3LocationData("FS: Uchigatana - NPC drop", "Uchigatana", hostile_npc=True), + DS3LocationData("FS: Master's Attire - NPC drop", "Master's Attire", hostile_npc=True), + DS3LocationData("FS: Master's Gloves - NPC drop", "Master's Gloves", hostile_npc=True), + + DS3LocationData("FS: Broken Straight Sword - gravestone after boss", + "Broken Straight Sword"), + DS3LocationData("FS: Homeward Bone - cliff edge after boss", "Homeward Bone"), + DS3LocationData("FS: Ember - path right of Firelink entrance", "Ember"), + DS3LocationData("FS: Soul of a Deserted Corpse - bell tower door", + "Soul of a Deserted Corpse"), + DS3LocationData("FS: East-West Shield - tree by shrine entrance", "East-West Shield"), + DS3LocationData("FS: Homeward Bone - path above shrine entrance", "Homeward Bone"), + DS3LocationData("FS: Ember - above shrine entrance", "Ember"), + DS3LocationData("FS: Wolf Ring+2 - left of boss room exit", "Wolf Ring+2", ngp=True), + # Leonhard (quest) + DS3LocationData("FS: Cracked Red Eye Orb - Leonhard", "Cracked Red Eye Orb x5", + missable=True, npc=True), + # Leonhard (kill or quest), missable because he can disappear sometimes + DS3LocationData("FS: Lift Chamber Key - Leonhard", "Lift Chamber Key", missable=True, + npc=True, drop=True), + + # Shrine Handmaid shop + DS3LocationData("FS: White Sign Soapstone - shop", "White Sign Soapstone", shop=True), + DS3LocationData("FS: Dried Finger - shop", "Dried Finger", shop=True), + DS3LocationData("FS: Tower Key - shop", "Tower Key", progression=True, shop=True), + DS3LocationData("FS: Ember - shop", "Ember", static='99,0:-1:110000:', shop=True), + DS3LocationData("FS: Farron Dart - shop", "Farron Dart", static='99,0:-1:110000:', + shop=True), + DS3LocationData("FS: Soul Arrow - shop", "Soul Arrow", static='99,0:-1:110000:', + shop=True), + DS3LocationData("FS: Heal Aid - shop", "Heal Aid", shop=True), + DS3LocationData("FS: Alluring Skull - Mortician's Ashes", "Alluring Skull", shop=True, + conditional=True), + DS3LocationData("FS: Ember - Mortician's Ashes", "Ember", + static='99,0:-1:110000,70000100:', shop=True, conditional=True), + DS3LocationData("FS: Grave Key - Mortician's Ashes", "Grave Key", shop=True, + conditional=True), + DS3LocationData("FS: Life Ring - Dreamchaser's Ashes", "Life Ring", shop=True, + conditional=True), + # Only if you say where the ashes were found + DS3LocationData("FS: Hidden Blessing - Dreamchaser's Ashes", "Hidden Blessing", + missable=True, shop=True), + DS3LocationData("FS: Lloyd's Shield Ring - Paladin's Ashes", "Lloyd's Shield Ring", + shop=True, conditional=True), + DS3LocationData("FS: Ember - Grave Warden's Ashes", "Ember", + static='99,0:-1:110000,70000103:', shop=True, conditional=True), + # Prisoner Chief's Ashes + DS3LocationData("FS: Karla's Pointed Hat - Prisoner Chief's Ashes", "Karla's Pointed Hat", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Coat - Prisoner Chief's Ashes", "Karla's Coat", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Gloves - Prisoner Chief's Ashes", "Karla's Gloves", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Trousers - Prisoner Chief's Ashes", "Karla's Trousers", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Xanthous Overcoat - Xanthous Ashes", "Xanthous Overcoat", shop=True, + conditional=True), + DS3LocationData("FS: Xanthous Gloves - Xanthous Ashes", "Xanthous Gloves", shop=True, + conditional=True), + DS3LocationData("FS: Xanthous Trousers - Xanthous Ashes", "Xanthous Trousers", shop=True, + conditional=True), + DS3LocationData("FS: Ember - Dragon Chaser's Ashes", "Ember", + static='99,0:-1:110000,70000108:', shop=True, conditional=True), + DS3LocationData("FS: Washing Pole - Easterner's Ashes", "Washing Pole", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Helm - Easterner's Ashes", "Eastern Helm", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Armor - Easterner's Ashes", "Eastern Armor", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Gauntlets - Easterner's Ashes", "Eastern Gauntlets", + shop=True, conditional=True), + DS3LocationData("FS: Eastern Leggings - Easterner's Ashes", "Eastern Leggings", shop=True, + conditional=True), + DS3LocationData("FS: Wood Grain Ring - Easterner's Ashes", "Wood Grain Ring", shop=True, + conditional=True), + DS3LocationData("FS: Millwood Knight Helm - Captain's Ashes", "Millwood Knight Helm", + dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Armor - Captain's Ashes", "Millwood Knight Armor", + dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Gauntlets - Captain's Ashes", + "Millwood Knight Gauntlets", dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Leggings - Captain's Ashes", + "Millwood Knight Leggings", dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Refined Gem - Captain's Ashes", "Refined Gem", dlc=True, shop=True, + conditional=True), + + # Ludleth Shop + DS3LocationData("FS: Vordt's Great Hammer - Ludleth for Vordt", "Vordt's Great Hammer", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Pontiff's Left Eye - Ludleth for Vordt", "Pontiff's Left Eye", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Bountiful Sunlight - Ludleth for Rosaria", "Bountiful Sunlight", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Darkmoon Longbow - Ludleth for Aldrich", "Darkmoon Longbow", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lifehunt Scythe - Ludleth for Aldrich", "Lifehunt Scythe", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Hollowslayer Greatsword - Ludleth for Greatwood", + "Hollowslayer Greatsword", missable=True, boss=True, shop=True), + DS3LocationData("FS: Arstor's Spear - Ludleth for Greatwood", "Arstor's Spear", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Crystal Sage's Rapier - Ludleth for Sage", "Crystal Sage's Rapier", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Crystal Hail - Ludleth for Sage", "Crystal Hail", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Cleric's Candlestick - Ludleth for Deacons", "Cleric's Candlestick", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Deep Soul - Ludleth for Deacons", "Deep Soul", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Havel's Ring - Ludleth for Stray Demon", "Havel's Ring", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Boulder Heave - Ludleth for Stray Demon", "Boulder Heave", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Farron Greatsword - Ludleth for Abyss Watchers", "Farron Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Wolf Knight's Greatsword - Ludleth for Abyss Watchers", + "Wolf Knight's Greatsword", missable=True, boss=True, shop=True), + DS3LocationData("FS: Wolnir's Holy Sword - Ludleth for Wolnir", "Wolnir's Holy Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Black Serpent - Ludleth for Wolnir", "Black Serpent", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Demon's Greataxe - Ludleth for Fire Demon", "Demon's Greataxe", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Demon's Fist - Ludleth for Fire Demon", "Demon's Fist", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Old King's Great Hammer - Ludleth for Old Demon King", + "Old King's Great Hammer", missable=True, boss=True, shop=True), + DS3LocationData("FS: Chaos Bed Vestiges - Ludleth for Old Demon King", "Chaos Bed Vestiges", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Greatsword of Judgment - Ludleth for Pontiff", + "Greatsword of Judgment", missable=True, boss=True, shop=True), + DS3LocationData("FS: Profaned Greatsword - Ludleth for Pontiff", "Profaned Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Yhorm's Great Machete - Ludleth for Yhorm", "Yhorm's Great Machete", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Yhorm's Greatshield - Ludleth for Yhorm", "Yhorm's Greatshield", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dancer's Enchanted Swords - Ludleth for Dancer", + "Dancer's Enchanted Swords", missable=True, boss=True, shop=True), + DS3LocationData("FS: Soothing Sunlight - Ludleth for Dancer", "Soothing Sunlight", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Greataxe - Ludleth for Dragonslayer", + "Dragonslayer Greataxe", missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Greatshield - Ludleth for Dragonslayer", + "Dragonslayer Greatshield", missable=True, boss=True, shop=True), + DS3LocationData("FS: Moonlight Greatsword - Ludleth for Oceiros", "Moonlight Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: White Dragon Breath - Ludleth for Oceiros", "White Dragon Breath", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lorian's Greatsword - Ludleth for Princes", "Lorian's Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lothric's Holy Sword - Ludleth for Princes", "Lothric's Holy Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Gundyr's Halberd - Ludleth for Champion", "Gundyr's Halberd", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Prisoner's Chain - Ludleth for Champion", "Prisoner's Chain", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Storm Curved Sword - Ludleth for Nameless", "Storm Curved Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Swordspear - Ludleth for Nameless", + "Dragonslayer Swordspear", missable=True, boss=True, shop=True), + DS3LocationData("FS: Lightning Storm - Ludleth for Nameless", "Lightning Storm", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Firelink Greatsword - Ludleth for Cinder", "Firelink Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Sunlight Spear - Ludleth for Cinder", "Sunlight Spear", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Friede's Great Scythe - Ludleth for Friede", "Friede's Great Scythe", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Rose of Ariandel - Ludleth for Friede", "Rose of Ariandel", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Demon's Scar - Ludleth for Demon Prince", "Demon's Scar", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Seething Chaos - Ludleth for Demon Prince", "Seething Chaos", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Frayed Blade - Ludleth for Midir", "Frayed Blade", missable=True, + dlc=True, boss=True, shop=True), + DS3LocationData("FS: Old Moonlight - Ludleth for Midir", "Old Moonlight", missable=True, + dlc=True, boss=True, shop=True), + DS3LocationData("FS: Gael's Greatsword - Ludleth for Gael", "Gael's Greatsword", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Repeating Crossbow - Ludleth for Gael", "Repeating Crossbow", + missable=True, dlc=True, boss=True, shop=True), ], "Firelink Shrine Bell Tower": [ - DS3LocationData("FSBT: Covetous Silver Serpent Ring", "Covetous Silver Serpent Ring", DS3LocationCategory.RING), - DS3LocationData("FSBT: Fire Keeper Robe", "Fire Keeper Robe", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Fire Keeper Gloves", "Fire Keeper Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Fire Keeper Skirt", "Fire Keeper Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Estus Ring", "Estus Ring", DS3LocationCategory.RING), - DS3LocationData("FSBT: Fire Keeper Soul", "Fire Keeper Soul", DS3LocationCategory.MISC), + # Guarded by Tower Key + DS3LocationData("FSBT: Homeward Bone - roof", "Homeward Bone x3"), + DS3LocationData("FSBT: Estus Ring - tower base", "Estus Ring"), + DS3LocationData("FSBT: Estus Shard - rafters", "Estus Shard"), + DS3LocationData("FSBT: Fire Keeper Soul - tower top", "Fire Keeper Soul"), + DS3LocationData("FSBT: Fire Keeper Robe - partway down tower", "Fire Keeper Robe"), + DS3LocationData("FSBT: Fire Keeper Gloves - partway down tower", "Fire Keeper Gloves"), + DS3LocationData("FSBT: Fire Keeper Skirt - partway down tower", "Fire Keeper Skirt"), + DS3LocationData("FSBT: Covetous Silver Serpent Ring - illusory wall past rafters", + "Covetous Silver Serpent Ring", hidden=True), + DS3LocationData("FSBT: Twinkling Titanite - lizard behind Firelink", + "Twinkling Titanite", lizard=True), + + # Mark all crow trades as missable since no one wants to have to try trading everything just + # in case it gives a progression item. + DS3LocationData("FSBT: Iron Bracelets - crow for Homeward Bone", "Iron Bracelets", + missable=True), + DS3LocationData("FSBT: Ring of Sacrifice - crow for Loretta's Bone", "Ring of Sacrifice", + missable=True), + DS3LocationData("FSBT: Porcine Shield - crow for Undead Bone Shard", "Porcine Shield", + missable=True), + DS3LocationData("FSBT: Lucatiel's Mask - crow for Vertebra Shackle", "Lucatiel's Mask", + missable=True), + DS3LocationData("FSBT: Very good! Carving - crow for Divine Blessing", + "Very good! Carving", missable=True), + DS3LocationData("FSBT: Thank you Carving - crow for Hidden Blessing", "Thank you Carving", + missable=True), + DS3LocationData("FSBT: I'm sorry Carving - crow for Shriving Stone", "I'm sorry Carving", + missable=True), + DS3LocationData("FSBT: Sunlight Shield - crow for Mendicant's Staff", "Sunlight Shield", + missable=True), + DS3LocationData("FSBT: Hollow Gem - crow for Eleonora", "Hollow Gem", + missable=True), + DS3LocationData("FSBT: Titanite Scale - crow for Blacksmith Hammer", "Titanite Scale x3", + static='99,0:50004330::', missable=True), + DS3LocationData("FSBT: Help me! Carving - crow for any sacred chime", "Help me! Carving", + missable=True), + DS3LocationData("FSBT: Titanite Slab - crow for Coiled Sword Fragment", "Titanite Slab", + missable=True), + DS3LocationData("FSBT: Hello Carving - crow for Alluring Skull", "Hello Carving", + missable=True), + DS3LocationData("FSBT: Armor of the Sun - crow for Siegbräu", "Armor of the Sun", + missable=True), + DS3LocationData("FSBT: Large Titanite Shard - crow for Firebomb", "Large Titanite Shard", + missable=True), + DS3LocationData("FSBT: Titanite Chunk - crow for Black Firebomb", "Titanite Chunk", + missable=True), + DS3LocationData("FSBT: Iron Helm - crow for Lightning Urn", "Iron Helm", missable=True), + DS3LocationData("FSBT: Twinkling Titanite - crow for Prism Stone", "Twinkling Titanite", + missable=True), + DS3LocationData("FSBT: Iron Leggings - crow for Seed of a Giant Tree", "Iron Leggings", + missable=True), + DS3LocationData("FSBT: Lightning Gem - crow for Xanthous Crown", "Lightning Gem", + missable=True), + DS3LocationData("FSBT: Twinkling Titanite - crow for Large Leather Shield", + "Twinkling Titanite", missable=True), + DS3LocationData("FSBT: Blessed Gem - crow for Moaning Shield", "Blessed Gem", + missable=True), ], "High Wall of Lothric": [ - DS3LocationData("HWL: Deep Battle Axe", "Deep Battle Axe", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Club", "Club", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Claymore", "Claymore", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Binoculars", "Binoculars", DS3LocationCategory.MISC), - DS3LocationData("HWL: Longbow", "Longbow", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Mail Breaker", "Mail Breaker", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Broadsword", "Broadsword", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Silver Eagle Kite Shield", "Silver Eagle Kite Shield", DS3LocationCategory.SHIELD), - DS3LocationData("HWL: Astora's Straight Sword", "Astora's Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Cell Key", "Cell Key", DS3LocationCategory.KEY), - DS3LocationData("HWL: Rapier", "Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Lucerne", "Lucerne", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Small Lothric Banner", "Small Lothric Banner", DS3LocationCategory.KEY), - DS3LocationData("HWL: Basin of Vows", "Basin of Vows", DS3LocationCategory.KEY), - DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", DS3LocationCategory.BOSS), - DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", DS3LocationCategory.BOSS), - DS3LocationData("HWL: Way of Blue", "Way of Blue", DS3LocationCategory.MISC), - DS3LocationData("HWL: Greirat's Ashes", "Greirat's Ashes", DS3LocationCategory.NPC), - DS3LocationData("HWL: Blue Tearstone Ring", "Blue Tearstone Ring", DS3LocationCategory.NPC), + DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", + prominent=True, boss=True), + DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", prominent=True, + boss=True), + DS3LocationData("HWL: Basin of Vows - Emma", "Basin of Vows", prominent=True, + progression=True, conditional=True), + DS3LocationData("HWL: Small Lothric Banner - Emma", "Small Lothric Banner", + prominent=True, progression=True), + DS3LocationData("HWL: Green Blossom - fort walkway, hall behind wheel", "Green Blossom x2", + hidden=True), + DS3LocationData("HWL: Gold Pine Resin - corpse tower, drop", "Gold Pine Resin x2", + hidden=True), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - flame plaza", + "Large Soul of a Deserted Corpse"), + DS3LocationData("HWL: Soul of a Deserted Corpse - by wall tower door", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Standard Arrow - back tower", "Standard Arrow x12"), + DS3LocationData("HWL: Longbow - back tower", "Longbow"), + DS3LocationData("HWL: Firebomb - wall tower, beam", "Firebomb x3"), + DS3LocationData("HWL: Throwing Knife - wall tower, path to Greirat", "Throwing Knife x8"), + DS3LocationData("HWL: Soul of a Deserted Corpse - corpse tower, bottom floor", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Club - flame plaza", "Club"), + DS3LocationData("HWL: Claymore - flame plaza", "Claymore"), + DS3LocationData("HWL: Ember - flame plaza", "Ember"), + DS3LocationData("HWL: Firebomb - corpse tower, under table", "Firebomb x2"), + DS3LocationData("HWL: Titanite Shard - wall tower, corner by bonfire", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Undead Hunter Charm - fort, room off entry, in pot", + "Undead Hunter Charm x2", hidden=True), + DS3LocationData("HWL: Firebomb - top of ladder to fountain", "Firebomb x3"), + DS3LocationData("HWL: Cell Key - fort ground, down stairs", "Cell Key"), + DS3LocationData("HWL: Ember - fountain #1", "Ember"), + DS3LocationData("HWL: Soul of a Deserted Corpse - fort entry, corner", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Lucerne - promenade, side path", "Lucerne"), + DS3LocationData("HWL: Mail Breaker - wall tower, path to Greirat", "Mail Breaker"), + DS3LocationData("HWL: Titanite Shard - fort ground behind crates", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Rapier - fountain, corner", "Rapier"), + DS3LocationData("HWL: Titanite Shard - fort, room off entry", "Titanite Shard"), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - fort roof", + "Large Soul of a Deserted Corpse"), + DS3LocationData("HWL: Black Firebomb - small roof over fountain", "Black Firebomb x3"), + DS3LocationData("HWL: Soul of a Deserted Corpse - path to corpse tower", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Ember - fountain #2", "Ember"), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - platform by fountain", + "Large Soul of a Deserted Corpse", hidden=True), # Easily missed turnoff + DS3LocationData("HWL: Binoculars - corpse tower, upper platform", "Binoculars"), + DS3LocationData("HWL: Ring of Sacrifice - awning by fountain", + "Ring of Sacrifice", hidden=True), # Easily missed turnoff + DS3LocationData("HWL: Throwing Knife - shortcut, lift top", "Throwing Knife x6"), + DS3LocationData("HWL: Soul of a Deserted Corpse - path to back tower, by lift door", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Green Blossom - shortcut, lower courtyard", "Green Blossom x3"), + DS3LocationData("HWL: Broadsword - fort, room off walkway", "Broadsword"), + DS3LocationData("HWL: Soul of a Deserted Corpse - fountain, path to promenade", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Firebomb - fort roof", "Firebomb x3"), + DS3LocationData("HWL: Soul of a Deserted Corpse - wall tower, right of exit", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Estus Shard - fort ground, on anvil", "Estus Shard"), + DS3LocationData("HWL: Fleshbite Ring+1 - fort roof, jump to other roof", + "Fleshbite Ring+1", ngp=True, hidden=True), # Hidden jump + DS3LocationData("HWL: Ring of the Evil Eye+2 - fort ground, far wall", + "Ring of the Evil Eye+2", ngp=True, hidden=True), # In barrels + DS3LocationData("HWL: Silver Eagle Kite Shield - fort mezzanine", + "Silver Eagle Kite Shield"), + DS3LocationData("HWL: Astora Straight Sword - fort walkway, drop down", + "Astora Straight Sword", hidden=True), # Hidden fall + DS3LocationData("HWL: Battle Axe - flame tower, mimic", "Battle Axe", + static='01,0:53000960::', mimic=True), + + # Only dropped after transformation + DS3LocationData("HWL: Ember - fort roof, transforming hollow", "Ember", hidden=True), + DS3LocationData("HWL: Titanite Shard - fort roof, transforming hollow", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Ember - back tower, transforming hollow", "Ember", hidden=True), + DS3LocationData("HWL: Titanite Shard - back tower, transforming hollow", "Titanite Shard", + hidden=True), + + DS3LocationData("HWL: Refined Gem - promenade miniboss", "Refined Gem", miniboss=True), + DS3LocationData("HWL: Way of Blue - Emma", "Way of Blue"), + # Categorize this as an NPC item so that it doesn't get randomized if the Lift Chamber Key + # isn't randomized, since in that case it's missable. + DS3LocationData("HWL: Red Eye Orb - wall tower, miniboss", "Red Eye Orb", + conditional=True, miniboss=True, npc=True), + DS3LocationData("HWL: Raw Gem - fort roof, lizard", "Raw Gem", lizard=True), ], "Undead Settlement": [ - DS3LocationData("US: Small Leather Shield", "Small Leather Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Whip", "Whip", DS3LocationCategory.WEAPON), - DS3LocationData("US: Reinforced Club", "Reinforced Club", DS3LocationCategory.WEAPON), - DS3LocationData("US: Blue Wooden Shield", "Blue Wooden Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Cleric Hat", "Cleric Hat", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Blue Robe", "Cleric Blue Robe", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Gloves", "Cleric Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Trousers", "Cleric Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mortician's Ashes", "Mortician's Ashes", DS3LocationCategory.KEY), - DS3LocationData("US: Caestus", "Caestus", DS3LocationCategory.WEAPON), - DS3LocationData("US: Plank Shield", "Plank Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Flame Stoneplate Ring", "Flame Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("US: Caduceus Round Shield", "Caduceus Round Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Fire Clutch Ring", "Fire Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("US: Partizan", "Partizan", DS3LocationCategory.WEAPON), - DS3LocationData("US: Bloodbite Ring", "Bloodbite Ring", DS3LocationCategory.RING), - DS3LocationData("US: Red Hilted Halberd", "Red Hilted Halberd", DS3LocationCategory.WEAPON), - DS3LocationData("US: Saint's Talisman", "Saint's Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("US: Irithyll Straight Sword", "Irithyll Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("US: Large Club", "Large Club", DS3LocationCategory.WEAPON), - DS3LocationData("US: Northern Helm", "Northern Helm", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Armor", "Northern Armor", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Gloves", "Northern Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Trousers", "Northern Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Flynn's Ring", "Flynn's Ring", DS3LocationCategory.RING), - DS3LocationData("US: Mirrah Vest", "Mirrah Vest", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mirrah Gloves", "Mirrah Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mirrah Trousers", "Mirrah Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Chloranthy Ring", "Chloranthy Ring", DS3LocationCategory.RING), - DS3LocationData("US: Loincloth", "Loincloth", DS3LocationCategory.ARMOR), - DS3LocationData("US: Wargod Wooden Shield", "Wargod Wooden Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Loretta's Bone", "Loretta's Bone", DS3LocationCategory.KEY), - DS3LocationData("US: Hand Axe", "Hand Axe", DS3LocationCategory.WEAPON), - DS3LocationData("US: Great Scythe", "Great Scythe", DS3LocationCategory.WEAPON), - DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", DS3LocationCategory.BOSS), - DS3LocationData("US: Hawk Ring", "Hawk Ring", DS3LocationCategory.RING), - DS3LocationData("US: Warrior of Sunlight", "Warrior of Sunlight", DS3LocationCategory.MISC), - DS3LocationData("US: Blessed Red and White Shield+1", "Blessed Red and White Shield+1", DS3LocationCategory.SHIELD), - DS3LocationData("US: Irina's Ashes", "Irina's Ashes", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Ashes", "Cornyx's Ashes", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Wrap", "Cornyx's Wrap", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Garb", "Cornyx's Garb", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Skirt", "Cornyx's Skirt", DS3LocationCategory.NPC), - DS3LocationData("US: Pyromancy Flame", "Pyromancy Flame", DS3LocationCategory.NPC), - DS3LocationData("US: Transposing Kiln", "Transposing Kiln", DS3LocationCategory.MISC), - DS3LocationData("US: Tower Key", "Tower Key", DS3LocationCategory.NPC), + DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", + prominent=True, boss=True), + DS3LocationData("US: Transposing Kiln - boss drop", "Transposing Kiln", boss=True), + # Missable because it's unavailable if you start as a Pyromancer + DS3LocationData("US: Pyromancy Flame - Cornyx", "Pyromancy Flame", missable=True, + npc=True), + DS3LocationData("US: Old Sage's Blindfold - kill Cornyx", "Old Sage's Blindfold", + npc=True), + DS3LocationData("US: Cornyx's Garb - kill Cornyx", "Cornyx's Garb", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Cornyx's Wrap - kill Cornyx", "Cornyx's Wrap", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Cornyx's Skirt - kill Cornyx", "Cornyx's Skirt", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Tower Key - kill Irina", "Tower Key", missable=True, npc=True), + DS3LocationData("US: Flynn's Ring - tower village, rooftop", "Flynn's Ring"), + DS3LocationData("US: Undead Bone Shard - by white tree", "Undead Bone Shard"), + DS3LocationData("US: Alluring Skull - foot, behind carriage", "Alluring Skull x2"), + DS3LocationData("US: Mortician's Ashes - graveyard by white tree", "Mortician's Ashes", + progression=True), + DS3LocationData("US: Homeward Bone - tower village, jump from roof", "Homeward Bone x2", + static='02,0:53100040::', hidden=True), # Hidden fall + DS3LocationData("US: Caduceus Round Shield - right after stable exit", + "Caduceus Round Shield"), + DS3LocationData("US: Ember - tower basement, miniboss", "Ember"), + DS3LocationData("US: Soul of an Unknown Traveler - chasm crypt", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Repair Powder - first building, balcony", "Repair Powder x2"), + DS3LocationData("US: Homeward Bone - stable roof", "Homeward Bone x2", + static='02,0:53100090::'), + DS3LocationData("US: Titanite Shard - back alley, side path", "Titanite Shard"), + DS3LocationData("US: Wargod Wooden Shield - Pit of Hollows", "Wargod Wooden Shield"), + DS3LocationData("US: Large Soul of a Deserted Corpse - on the way to tower, by well", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Ember - bridge on the way to tower", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - stable", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Titanite Shard - porch after burning tree", "Titanite Shard"), + DS3LocationData("US: Alluring Skull - tower village building, upstairs", + "Alluring Skull x2"), + DS3LocationData("US: Charcoal Pine Bundle - first building, middle floor", + "Charcoal Pine Bundle x2"), + DS3LocationData("US: Blue Wooden Shield - graveyard by white tree", "Blue Wooden Shield"), + DS3LocationData("US: Cleric Hat - graveyard by white tree", "Cleric Hat"), + DS3LocationData("US: Cleric Blue Robe - graveyard by white tree", "Cleric Blue Robe"), + DS3LocationData("US: Cleric Gloves - graveyard by white tree", "Cleric Gloves"), + DS3LocationData("US: Cleric Trousers - graveyard by white tree", "Cleric Trousers"), + DS3LocationData("US: Soul of an Unknown Traveler - portcullis by burning tree", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Charcoal Pine Resin - hanging corpse room", "Charcoal Pine Resin x2"), + DS3LocationData("US: Loincloth - by Velka statue", "Loincloth"), + DS3LocationData("US: Bloodbite Ring - miniboss in sewer", "Bloodbite Ring", + miniboss=True), # Giant Rat drop + DS3LocationData("US: Charcoal Pine Bundle - first building, bottom floor", + "Charcoal Pine Bundle x2"), + DS3LocationData("US: Soul of an Unknown Traveler - back alley, past crates", + "Soul of an Unknown Traveler", hidden=True), + DS3LocationData("US: Titanite Shard - back alley, up ladder", "Titanite Shard"), + DS3LocationData("US: Red Hilted Halberd - chasm crypt", "Red Hilted Halberd"), + DS3LocationData("US: Rusted Coin - awning above Dilapidated Bridge", "Rusted Coin x2"), + DS3LocationData("US: Caestus - sewer", "Caestus"), + DS3LocationData("US: Saint's Talisman - chasm, by ladder", "Saint's Talisman"), + DS3LocationData("US: Alluring Skull - on the way to tower, behind building", + "Alluring Skull x3"), + DS3LocationData("US: Large Club - tower village, by miniboss", "Large Club"), + DS3LocationData("US: Titanite Shard - chasm #1", "Titanite Shard"), + DS3LocationData("US: Titanite Shard - chasm #2", "Titanite Shard"), + DS3LocationData("US: Fading Soul - outside stable", "Fading Soul"), + DS3LocationData("US: Titanite Shard - lower path to Cliff Underside", "Titanite Shard", + hidden=True), # hidden fall + DS3LocationData("US: Hand Axe - by Cornyx", "Hand Axe"), + DS3LocationData("US: Soul of an Unknown Traveler - pillory past stable", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Ember - by stairs to boss", "Ember"), + DS3LocationData("US: Mirrah Vest - tower village, jump from roof", "Mirrah Vest", + hidden=True), # Hidden fall + DS3LocationData("US: Mirrah Gloves - tower village, jump from roof", "Mirrah Gloves", + hidden=True), # Hidden fall + DS3LocationData("US: Mirrah Trousers - tower village, jump from roof", "Mirrah Trousers", + hidden=True), # Hidden fall + DS3LocationData("US: Plank Shield - outside stable, by NPC", "Plank Shield"), + DS3LocationData("US: Red Bug Pellet - tower village building, basement", + "Red Bug Pellet x2"), + DS3LocationData("US: Chloranthy Ring - tower village, jump from roof", "Chloranthy Ring", + hidden=True), # Hidden fall + DS3LocationData("US: Fire Clutch Ring - wooden walkway past stable", "Fire Clutch Ring"), + DS3LocationData("US: Estus Shard - under burning tree", "Estus Shard"), + DS3LocationData("US: Firebomb - stable roof", "Firebomb x6"), + # In enemy rando, the enemy may not burst through the wall and make this room obvious + DS3LocationData("US: Whip - back alley, behind wooden wall", "Whip", hidden=True), + DS3LocationData("US: Great Scythe - building by white tree, balcony", "Great Scythe"), + DS3LocationData("US: Homeward Bone - foot, drop overlook", "Homeward Bone", + static='02,0:53100540::'), + DS3LocationData("US: Large Soul of a Deserted Corpse - around corner by Cliff Underside", + "Large Soul of a Deserted Corpse", hidden=True), # Hidden corner + DS3LocationData("US: Ember - behind burning tree", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - across from Foot of the High Wall", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Fading Soul - by white tree", "Fading Soul"), + DS3LocationData("US: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("US: Ember - by white tree", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - by white tree", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("US: Reinforced Club - by white tree", "Reinforced Club"), + DS3LocationData("US: Soul of a Nameless Soldier - top of tower", + "Soul of a Nameless Soldier"), + DS3LocationData("US: Loretta's Bone - first building, hanging corpse on balcony", + "Loretta's Bone"), + DS3LocationData("US: Northern Helm - tower village, hanging corpse", "Northern Helm"), + DS3LocationData("US: Northern Armor - tower village, hanging corpse", "Northern Armor"), + DS3LocationData("US: Northern Gloves - tower village, hanging corpse", "Northern Gloves"), + DS3LocationData("US: Northern Trousers - tower village, hanging corpse", + "Northern Trousers"), + DS3LocationData("US: Partizan - hanging corpse above Cliff Underside", "Partizan", + missable=True), # requires projectile + DS3LocationData("US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transport", + "Flame Stoneplate Ring"), + DS3LocationData("US: Red and White Shield - chasm, hanging corpse", "Red and White Shield", + static="02,0:53100740::", missable=True), # requires projectile + DS3LocationData("US: Small Leather Shield - first building, hanging corpse by entrance", + "Small Leather Shield"), + DS3LocationData("US: Pale Tongue - tower village, hanging corpse", "Pale Tongue"), + DS3LocationData("US: Large Soul of a Deserted Corpse - hanging corpse room, over stairs", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Kukri - hanging corpse above burning tree", "Kukri x9", + missable=True), # requires projectile + DS3LocationData("US: Life Ring+1 - tower on the way to village", "Life Ring+1", ngp=True), + DS3LocationData("US: Poisonbite Ring+1 - graveyard by white tree, near well", + "Poisonbite Ring+1", ngp=True), + DS3LocationData("US: Covetous Silver Serpent Ring+2 - tower village, drop down from roof", + "Covetous Silver Serpent Ring+2", ngp=True, hidden=True), # Hidden fall + DS3LocationData("US: Human Pine Resin - tower village building, chest upstairs", + "Human Pine Resin x4"), + DS3LocationData("US: Homeward Bone - tower village, right at start", "Homeward Bone", + static='02,0:53100540::'), + DS3LocationData("US: Irithyll Straight Sword - miniboss drop, by Road of Sacrifices", + "Irithyll Straight Sword", miniboss=True), + DS3LocationData("US: Fire Gem - tower village, miniboss drop", "Fire Gem", miniboss=True), + DS3LocationData("US: Warrior of Sunlight - hanging corpse room, drop through hole", + "Warrior of Sunlight", hidden=True), # hidden fall + DS3LocationData("US: Mound-makers - Hodrick", "Mound-makers", missable=True), + DS3LocationData("US: Sharp Gem - lizard by Dilapidated Bridge", "Sharp Gem", lizard=True), + DS3LocationData("US: Heavy Gem - chasm, lizard", "Heavy Gem", lizard=True), + DS3LocationData("US: Siegbräu - Siegward", "Siegbräu", missable=True, npc=True), + DS3LocationData("US: Heavy Gem - Hawkwood", "Heavy Gem", static='00,0:50006070::', + missable=True, npc=True), # Hawkwood (quest, after Greatwood or Sage) + DS3LocationData("US -> RS", None), + + # Yoel/Yuria of Londor + DS3LocationData("FS: Soul Arrow - Yoel/Yuria", "Soul Arrow", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Heavy Soul Arrow - Yoel/Yuria", "Heavy Soul Arrow", + static='99,0:-1:50000,110000,70000116:', + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Weapon - Yoel/Yuria", "Magic Weapon", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Magic Shield - Yoel/Yuria", "Magic Shield", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Greatsword - Yoel/Yuria", "Soul Greatsword", + static='99,0:-1:50000,110000,70000450,70000475:', missable=True, + npc=True, shop=True), + DS3LocationData("FS: Dark Hand - Yoel/Yuria", "Dark Hand", missable=True, npc=True), + DS3LocationData("FS: Untrue White Ring - Yoel/Yuria", "Untrue White Ring", missable=True, + npc=True), + DS3LocationData("FS: Untrue Dark Ring - Yoel/Yuria", "Untrue Dark Ring", missable=True, + npc=True), + DS3LocationData("FS: Londor Braille Divine Tome - Yoel/Yuria", "Londor Braille Divine Tome", + static='99,0:-1:40000,110000,70000116:', missable=True, npc=True), + DS3LocationData("FS: Darkdrift - Yoel/Yuria", "Darkdrift", missable=True, drop=True, + npc=True), # kill her or kill Soul of Cinder + + # Cornyx of the Great Swamp + # These aren't missable because the Shrine Handmaid will carry them if you kill Cornyx. + DS3LocationData("FS: Fireball - Cornyx", "Fireball", npc=True, shop=True), + DS3LocationData("FS: Fire Surge - Cornyx", "Fire Surge", npc=True, shop=True), + DS3LocationData("FS: Great Combustion - Cornyx", "Great Combustion", npc=True, + shop=True), + DS3LocationData("FS: Flash Sweat - Cornyx", "Flash Sweat", npc=True, shop=True), + # These are missable if you kill Cornyx before giving him the right tomes. + DS3LocationData("FS: Poison Mist - Cornyx for Great Swamp Tome", "Poison Mist", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Fire Orb - Cornyx for Great Swamp Tome", "Fire Orb", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Profuse Sweat - Cornyx for Great Swamp Tome", "Profuse Sweat", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Bursting Fireball - Cornyx for Great Swamp Tome", "Bursting Fireball", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Acid Surge - Cornyx for Carthus Tome", "Acid Surge", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Carthus Flame Arc - Cornyx for Carthus Tome", "Carthus Flame Arc", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Carthus Beacon - Cornyx for Carthus Tome", "Carthus Beacon", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Great Chaos Fire Orb - Cornyx for Izalith Tome", + "Great Chaos Fire Orb", missable=True, npc=True, shop=True), + DS3LocationData("FS: Chaos Storm - Cornyx for Izalith Tome", "Chaos Storm", missable=True, + npc=True, shop=True), + + # Irina of Carim + # These aren't in their own location because you don't actually need the Grave Key to access + # Irena—you can just fall down the cliff near Eygon. + DS3LocationData("FS: Saint's Ring - Irina", "Saint's Ring", npc=True, shop=True), + DS3LocationData("FS: Heal - Irina", "Heal", npc=True, shop=True), + DS3LocationData("FS: Replenishment - Irina", "Replenishment", npc=True, shop=True), + DS3LocationData("FS: Caressing Tears - Irina", "Caressing Tears", npc=True, shop=True), + DS3LocationData("FS: Homeward - Irina", "Homeward", npc=True, shop=True), + DS3LocationData("FS: Med Heal - Irina for Tome of Carim", "Med Heal", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Tears of Denial - Irina for Tome of Carim", "Tears of Denial", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Force - Irina for Tome of Carim", "Force", missable=True, npc=True, + shop=True), + DS3LocationData("FS: Bountiful Light - Irina for Tome of Lothric", "Bountiful Light", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Barrier - Irina for Tome of Lothric", "Magic Barrier", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Blessed Weapon - Irina for Tome of Lothric", "Blessed Weapon", + missable=True, npc=True, shop=True), ], "Road of Sacrifices": [ - DS3LocationData("RS: Brigand Twindaggers", "Brigand Twindaggers", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Brigand Hood", "Brigand Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Armor", "Brigand Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Gauntlets", "Brigand Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Trousers", "Brigand Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Butcher Knife", "Butcher Knife", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Brigand Axe", "Brigand Axe", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Braille Divine Tome of Carim", "Braille Divine Tome of Carim", DS3LocationCategory.MISC), - DS3LocationData("RS: Morne's Ring", "Morne's Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Twin Dragon Greatshield", "Twin Dragon Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Heretic's Staff", "Heretic's Staff", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Sorcerer Hood", "Sorcerer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Robe", "Sorcerer Robe", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Gloves", "Sorcerer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Trousers", "Sorcerer Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sage Ring", "Sage Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Fallen Knight Helm", "Fallen Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Armor", "Fallen Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Gauntlets", "Fallen Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Trousers", "Fallen Knight Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Hood", "Conjurator Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Robe", "Conjurator Robe", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Manchettes", "Conjurator Manchettes", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Boots", "Conjurator Boots", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Great Swamp Pyromancy Tome", "Great Swamp Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("RS: Great Club", "Great Club", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Exile Greatsword", "Exile Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Farron Coal", "Farron Coal", DS3LocationCategory.MISC), - DS3LocationData("RS: Sellsword Twinblades", "Sellsword Twinblades", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Sellsword Helm", "Sellsword Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Armor", "Sellsword Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Gauntlet", "Sellsword Gauntlet", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Trousers", "Sellsword Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Golden Falcon Shield", "Golden Falcon Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Herald Helm", "Herald Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Armor", "Herald Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Gloves", "Herald Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Trousers", "Herald Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Grass Crest Shield", "Grass Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", DS3LocationCategory.BOSS), - DS3LocationData("RS: Great Swamp Ring", "Great Swamp Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Orbeck's Ashes", "Orbeck's Ashes", DS3LocationCategory.NPC), + DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", prominent=True, + boss=True), + DS3LocationData("RS: Exile Greatsword - NPC drop by Farron Keep", "Exile Greatsword", + hostile_npc=True), # Exile Knight #2 drop + DS3LocationData("RS: Great Club - NPC drop by Farron Keep", "Great Club", + hostile_npc=True), # Exile Knight #1 drop + DS3LocationData("RS: Heysel Pick - Heysel drop", "Heysel Pick", missable=True, + hostile_npc=True), + DS3LocationData("RS: Xanthous Crown - Heysel drop", "Xanthous Crown", missable=True, + hostile_npc=True), + DS3LocationData("RS: Butcher Knife - NPC drop beneath road", "Butcher Knife", + hostile_npc=True), # Madwoman + DS3LocationData("RS: Titanite Shard - water by Halfway Fortress", "Titanite Shard"), + DS3LocationData("RS: Titanite Shard - woods, left of path from Halfway Fortress", + "Titanite Shard"), + DS3LocationData("RS: Green Blossom - by deep water", "Green Blossom x4"), + DS3LocationData("RS: Estus Shard - left of fire behind stronghold left room", + "Estus Shard"), + DS3LocationData("RS: Ring of Sacrifice - stronghold, drop from right room balcony", + "Ring of Sacrifice", hidden=True), # hidden fall + DS3LocationData("RS: Soul of an Unknown Traveler - drop along wall from Halfway Fortress", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Fallen Knight Helm - water's edge by Farron Keep", + "Fallen Knight Helm"), + DS3LocationData("RS: Fallen Knight Armor - water's edge by Farron Keep", + "Fallen Knight Armor"), + DS3LocationData("RS: Fallen Knight Gauntlets - water's edge by Farron Keep", + "Fallen Knight Gauntlets"), + DS3LocationData("RS: Fallen Knight Trousers - water's edge by Farron Keep", + "Fallen Knight Trousers"), + DS3LocationData("RS: Heretic's Staff - stronghold left room", "Heretic's Staff"), + DS3LocationData("RS: Large Soul of an Unknown Traveler - left of stairs to Farron Keep", + "Large Soul of an Unknown Traveler"), + DS3LocationData("RS: Conjurator Hood - deep water", "Conjurator Hood"), + DS3LocationData("RS: Conjurator Robe - deep water", "Conjurator Robe"), + DS3LocationData("RS: Conjurator Manchettes - deep water", "Conjurator Manchettes"), + DS3LocationData("RS: Conjurator Boots - deep water", "Conjurator Boots"), + DS3LocationData("RS: Soul of an Unknown Traveler - right of door to stronghold left", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Green Blossom - water beneath stronghold", "Green Blossom x2"), + DS3LocationData("RS: Great Swamp Pyromancy Tome - deep water", + "Great Swamp Pyromancy Tome"), + DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"), + DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"), + DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire", + "Twin Dragon Greatshield"), + DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Gloves - water beneath stronghold", "Sorcerer Gloves", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Trousers - water beneath stronghold", "Sorcerer Trousers", + hidden=True), # Hidden fall + DS3LocationData("RS: Sage Ring - water beneath stronghold", "Sage Ring", + hidden=True), # Hidden fall + DS3LocationData("RS: Grass Crest Shield - water by Crucifixion Woods bonfire", + "Grass Crest Shield"), + DS3LocationData("RS: Ember - right of fire behind stronghold left room", "Ember"), + DS3LocationData("RS: Blue Bug Pellet - broken stairs by Orbeck", "Blue Bug Pellet x2"), + DS3LocationData("RS: Soul of an Unknown Traveler - road, by wagon", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Shriving Stone - road, by start", "Shriving Stone"), + DS3LocationData("RS: Titanite Shard - road, on bridge after you go under", + "Titanite Shard"), + DS3LocationData("RS: Brigand Twindaggers - beneath road", "Brigand Twindaggers"), + DS3LocationData("RS: Braille Divine Tome of Carim - drop from bridge to Halfway Fortress", + "Braille Divine Tome of Carim", hidden=True), # Hidden fall + DS3LocationData("RS: Ember - right of Halfway Fortress entrance", "Ember"), + DS3LocationData("RS: Sellsword Twinblades - keep perimeter", "Sellsword Twinblades"), + DS3LocationData("RS: Golden Falcon Shield - path from stronghold right room to Farron Keep", + "Golden Falcon Shield"), + DS3LocationData("RS: Brigand Axe - beneath road", "Brigand Axe"), + DS3LocationData("RS: Brigand Hood - beneath road", "Brigand Hood"), + DS3LocationData("RS: Brigand Armor - beneath road", "Brigand Armor"), + DS3LocationData("RS: Brigand Gauntlets - beneath road", "Brigand Gauntlets"), + DS3LocationData("RS: Brigand Trousers - beneath road", "Brigand Trousers"), + DS3LocationData("RS: Morne's Ring - drop from bridge to Halfway Fortress", "Morne's Ring", + hidden=True), # Hidden fall + DS3LocationData("RS: Sellsword Helm - keep perimeter balcony", "Sellsword Helm"), + DS3LocationData("RS: Sellsword Armor - keep perimeter balcony", "Sellsword Armor"), + DS3LocationData("RS: Sellsword Gauntlet - keep perimeter balcony", "Sellsword Gauntlet"), + DS3LocationData("RS: Sellsword Trousers - keep perimeter balcony", "Sellsword Trousers"), + DS3LocationData("RS: Farron Coal - keep perimeter", "Farron Coal"), + DS3LocationData("RS: Chloranthy Ring+2 - road, drop across from carriage", + "Chloranthy Ring+2", hidden=True, ngp=True), # Hidden fall + DS3LocationData("RS: Lingering Dragoncrest Ring+1 - water", "Lingering Dragoncrest Ring+1", + ngp=True), + DS3LocationData("RS: Great Swamp Ring - miniboss drop, by Farron Keep", + "Great Swamp Ring", miniboss=True), # Giant Crab drop + DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels", + missable=True, npc=True), # Horace quest + DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem"), + DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul", + static='03,0:53300210::'), + + # Orbeck shop, all missable because he'll disappear if you don't talk to him for too long or + # if you don't give him a scroll. + DS3LocationData("FS: Farron Dart - Orbeck", "Farron Dart", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Arrow - Orbeck", "Soul Arrow", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Great Soul Arrow - Orbeck", "Great Soul Arrow", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Heavy Soul Arrow - Orbeck", "Heavy Soul Arrow", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Great Heavy Soul Arrow - Orbeck", "Great Heavy Soul Arrow", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Weapon - Orbeck", "Magic Weapon", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Magic Shield - Orbeck", "Magic Shield", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Spook - Orbeck", "Spook", missable=True, npc=True, shop=True), + DS3LocationData("FS: Aural Decoy - Orbeck", "Aural Decoy", missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Greatsword - Orbeck", "Soul Greatsword", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True), + DS3LocationData("FS: Farron Flashsword - Orbeck", "Farron Flashsword", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Pestilent Mist - Orbeck for any scroll", "Pestilent Mist", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Great Farron Dart - Orbeck for Sage's Scroll", "Great Farron Dart", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Farron Hail - Orbeck for Sage's Scroll", "Farron Hail", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Homing Soulmass - Orbeck for Logan's Scroll", "Homing Soulmass", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Soul Spear - Orbeck for Logan's Scroll", "Soul Spear", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Homing Crystal Soulmass - Orbeck for Crystal Scroll", + "Homing Crystal Soulmass", missable=True, npc=True, shop=True), + DS3LocationData("FS: Crystal Soul Spear - Orbeck for Crystal Scroll", "Crystal Soul Spear", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Crystal Magic Weapon - Orbeck for Crystal Scroll", + "Crystal Magic Weapon", missable=True, npc=True, shop=True), + DS3LocationData("FS: Cast Light - Orbeck for Golden Scroll", "Cast Light", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Twisted Wall of Light - Orbeck for Golden Scroll", + "Twisted Wall of Light", missable=True, npc=True, shop=True), + DS3LocationData("FS: Hidden Weapon - Orbeck for Golden Scroll", "Hidden Weapon", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Hidden Body - Orbeck for Golden Scroll", "Hidden Body", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Repair - Orbeck for Golden Scroll", "Repair", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Clandestine Coat - shop with Orbeck's Ashes", "Clandestine Coat", + missable=True, npc=True, + shop=True), # Shrine Handmaid with Orbeck's Ashes + reload + DS3LocationData("FS: Young Dragon Ring - Orbeck for one scroll and buying three spells", + "Young Dragon Ring", missable=True, npc=True), + DS3LocationData("FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spells", + "Slumbering Dragoncrest Ring", missable=True, npc=True), + DS3LocationData("RS -> FK", None), + + # Shrine Handmaid after killing exiles + DS3LocationData("FS: Exile Mask - shop after killing NPCs in RS", "Exile Mask", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Armor - shop after killing NPCs in RS", "Exile Armor", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Gauntlets - shop after killing NPCs in RS", "Exile Gauntlets", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Leggings - shop after killing NPCs in RS", "Exile Leggings", + hostile_npc=True, shop=True, hidden=True), + + # Shrine Handmaid after killing Crystal Sage + DS3LocationData("FS: Sage's Big Hat - shop after killing RS boss", "Sage's Big Hat", + boss=True, shop=True), + + # Yuria of Londor for Orbeck's Ashes + DS3LocationData("FS: Morion Blade - Yuria for Orbeck's Ashes", "Morion Blade", + missable=True, npc=True), ], "Cathedral of the Deep": [ - DS3LocationData("CD: Paladin's Ashes", "Paladin's Ashes", DS3LocationCategory.MISC), - DS3LocationData("CD: Spider Shield", "Spider Shield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Crest Shield", "Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Notched Whip", "Notched Whip", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Astora Greatsword", "Astora Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Executioner's Greatsword", "Executioner's Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Curse Ward Greatshield", "Curse Ward Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Saint-tree Bellvine", "Saint-tree Bellvine", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Poisonbite Ring", "Poisonbite Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Lloyd's Sword Ring", "Lloyd's Sword Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Seek Guidance", "Seek Guidance", DS3LocationCategory.SPELL), - DS3LocationData("CD: Aldrich's Sapphire", "Aldrich's Sapphire", DS3LocationCategory.RING), - DS3LocationData("CD: Deep Braille Divine Tome", "Deep Braille Divine Tome", DS3LocationCategory.MISC), - DS3LocationData("CD: Saint Bident", "Saint Bident", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Maiden Hood", "Maiden Hood", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Robe", "Maiden Robe", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Gloves", "Maiden Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Skirt", "Maiden Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Armor", "Drang Armor", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Gauntlets", "Drang Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Shoes", "Drang Shoes", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Hammers", "Drang Hammers", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Deep Ring", "Deep Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Archdeacon White Crown", "Archdeacon White Crown", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Archdeacon Holy Garb", "Archdeacon Holy Garb", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Archdeacon Skirt", "Archdeacon Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Arbalest", "Arbalest", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Small Doll", "Small Doll", DS3LocationCategory.KEY), - DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", DS3LocationCategory.BOSS), - DS3LocationData("CD: Rosaria's Fingers", "Rosaria's Fingers", DS3LocationCategory.MISC) + DS3LocationData("CD: Herald Helm - path, by fire", "Herald Helm"), + DS3LocationData("CD: Herald Armor - path, by fire", "Herald Armor"), + DS3LocationData("CD: Herald Gloves - path, by fire", "Herald Gloves"), + DS3LocationData("CD: Herald Trousers - path, by fire", "Herald Trousers"), + DS3LocationData("CD: Twinkling Titanite - path, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Twinkling Titanite - path, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Small Doll - boss drop", "Small Doll", prominent=True, + progression=True, boss=True), + DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", + boss=True), + DS3LocationData("CD: Black Eye Orb - Rosaria from Leonhard's quest", "Black Eye Orb", + missable=True, npc=True), + DS3LocationData("CD: Winged Spear - kill Patches", "Winged Spear", drop=True, + missable=True), # Patches (kill) + DS3LocationData("CD: Spider Shield - NPC drop on path", "Spider Shield", + hostile_npc=True), # Brigand + DS3LocationData("CD: Notched Whip - Cleansing Chapel", "Notched Whip"), + DS3LocationData("CD: Titanite Shard - Cleansing Chapel windowsill, by miniboss", + "Titanite Shard"), + DS3LocationData("CD: Astora Greatsword - graveyard, left of entrance", "Astora Greatsword"), + DS3LocationData("CD: Executioner's Greatsword - graveyard, far end", + "Executioner's Greatsword"), + DS3LocationData("CD: Undead Bone Shard - gravestone by white tree", "Undead Bone Shard"), + DS3LocationData("CD: Curse Ward Greatshield - by ladder from white tree to moat", + "Curse Ward Greatshield"), + DS3LocationData("CD: Titanite Shard - moat, far end", "Titanite Shard"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balcony", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Paladin's Ashes - path, guarded by lower NPC", "Paladin's Ashes", + progression=True), + DS3LocationData("CD: Arbalest - upper roofs, end of furthest buttress", "Arbalest"), + DS3LocationData("CD: Ember - by back door", "Ember"), + DS3LocationData("CD: Ember - side chapel upstairs, up ladder", "Ember"), + DS3LocationData("CD: Poisonbite Ring - moat, hall past miniboss", "Poisonbite Ring"), + DS3LocationData("CD: Drang Armor - main hall, east", "Drang Armor"), + DS3LocationData("CD: Ember - edge of platform before boss", "Ember"), + DS3LocationData("CD: Duel Charm - next to Patches in onion armor", "Duel Charm x3"), + DS3LocationData("CD: Seek Guidance - side chapel upstairs", "Seek Guidance"), + DS3LocationData("CD: Estus Shard - monument outside Cleansing Chapel", "Estus Shard"), + DS3LocationData("CD: Maiden Hood - main hall south", "Maiden Hood"), + DS3LocationData("CD: Maiden Robe - main hall south", "Maiden Robe"), + DS3LocationData("CD: Maiden Gloves - main hall south", "Maiden Gloves"), + DS3LocationData("CD: Maiden Skirt - main hall south", "Maiden Skirt"), + DS3LocationData("CD: Pale Tongue - upper roofs, outdoors far end", "Pale Tongue"), + DS3LocationData("CD: Fading Soul - graveyard, far end", "Fading Soul"), + DS3LocationData("CD: Blessed Gem - upper roofs, rafters", "Blessed Gem"), + DS3LocationData("CD: Red Bug Pellet - right of cathedral front doors", "Red Bug Pellet"), + DS3LocationData("CD: Soul of a Nameless Soldier - main hall south", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Duel Charm - by first elevator", "Duel Charm"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - main hall south, side path", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Ember - side chapel, miniboss room", "Ember"), + DS3LocationData("CD: Repair Powder - by white tree", "Repair Powder x3"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - by white tree #1", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - by white tree #2", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Undead Hunter Charm - lower roofs, up stairs between buttresses", + "Undead Hunter Charm x3"), + DS3LocationData("CD: Red Bug Pellet - lower roofs, up stairs between buttresses", + "Red Bug Pellet x3"), + DS3LocationData("CD: Titanite Shard - outside building by white tree", "Titanite Shard", + hidden=True), # Easily missable side path + DS3LocationData("CD: Titanite Shard - moat, up a slope", "Titanite Shard"), + DS3LocationData("CD: Rusted Coin - left of cathedral front doors, behind crates", + "Rusted Coin x2", hidden=True), + DS3LocationData("CD: Drang Hammers - main hall east", "Drang Hammers"), + DS3LocationData("CD: Drang Shoes - main hall east", "Drang Shoes"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - main hall east", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Pale Tongue - main hall east", "Pale Tongue"), + DS3LocationData("CD: Drang Gauntlets - main hall east", "Drang Gauntlets"), + DS3LocationData("CD: Soul of a Nameless Soldier - lower roofs, side room", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Exploding Bolt - ledge above main hall south", "Exploding Bolt x6"), + DS3LocationData("CD: Lloyd's Sword Ring - ledge above main hall south", + "Lloyd's Sword Ring"), + DS3LocationData("CD: Soul of a Nameless Soldier - ledge above main hall south", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Homeward Bone - outside main hall south door", "Homeward Bone x2"), + DS3LocationData("CD: Deep Gem - down stairs by first elevator", "Deep Gem"), + DS3LocationData("CD: Titanite Shard - path, side path by Cathedral of the Deep bonfire", + "Titanite Shard"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - path, against outer wall", + "Large Soul of an Unknown Traveler"), + # Before the stairs leading down into the Deacons fight + DS3LocationData("CD: Ring of the Evil Eye+1 - by stairs to boss", "Ring of the Evil Eye+1", + ngp=True), + DS3LocationData("CD: Ring of Favor+2 - upper roofs, on buttress", "Ring of Favor+2", + hidden=True, ngp=True), # Hidden fall + DS3LocationData("CD: Crest Shield - path, drop down by Cathedral of the Deep bonfire", + "Crest Shield", hidden=True), # Hidden fall + DS3LocationData("CD: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("CD: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("CD: Saint-tree Bellvine - moat, by water", "Saint-tree Bellvine"), + DS3LocationData("CD: Saint Bident - outside main hall south door", "Saint Bident"), + # Archdeacon set is hidden because you have to return to a cleared area + DS3LocationData("CD: Archdeacon White Crown - boss room after killing boss", + "Archdeacon White Crown", boss=True, hidden=True), + DS3LocationData("CD: Archdeacon Holy Garb - boss room after killing boss", + "Archdeacon Holy Garb", boss=True, hidden=True), + DS3LocationData("CD: Archdeacon Skirt - boss room after killing boss", "Archdeacon Skirt", + boss=True, hidden=True), + # Heysel items may not be missable, but it's not clear what causes them to trigger + DS3LocationData("CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed Chamber", + "Heysel Pick", missable=True), + DS3LocationData("CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed Chamber", + "Xanthous Crown", missable=True), + DS3LocationData("CD: Deep Ring - upper roofs, passive mob drop in first tower", "Deep Ring", + drop=True, hidden=True), + DS3LocationData("CD: Deep Braille Divine Tome - mimic by side chapel", + "Deep Braille Divine Tome", mimic=True), + DS3LocationData("CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed Chamber", + "Red Sign Soapstone", drop=True, hidden=True), + DS3LocationData("CD: Aldrich's Sapphire - side chapel, miniboss drop", "Aldrich's Sapphire", + miniboss=True), # Deep Accursed Drop + DS3LocationData("CD: Titanite Scale - moat, miniboss drop", "Titanite Scale", + miniboss=True), # Ravenous Crystal Lizard drop + DS3LocationData("CD: Twinkling Titanite - moat, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Twinkling Titanite - moat, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Rosaria's Fingers - Rosaria", "Rosaria's Fingers", + hidden=True), # Hidden fall + DS3LocationData("CD -> PW1", None), + + # Longfinger Kirk drops + DS3LocationData("CD: Barbed Straight Sword - Kirk drop", "Barbed Straight Sword", + missable=True, hostile_npc=True), + DS3LocationData("CD: Spiked Shield - Kirk drop", "Spiked Shield", missable=True, + hostile_npc=True), + # In Rosaria's Bed Chamber + DS3LocationData("CD: Helm of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Helm of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Armor of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Armor of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Gauntlets of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Leggings of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Leggings of Thorns", missable=True, hostile_npc=True), + + # Unbreakable Patches + DS3LocationData("CD: Rusted Coin - don't forgive Patches", "Rusted Coin", + missable=True, npc=True), + DS3LocationData("FS: Rusted Gold Coin - don't forgive Patches", "Rusted Gold Coin", + static='99,0:50006201::', missable=True, + npc=True), # Don't forgive Patches + DS3LocationData("CD: Shotel - Patches", "Shotel", missable=True, npc=True, shop=True), + DS3LocationData("CD: Ember - Patches", "Ember", missable=True, npc=True, shop=True), + DS3LocationData("CD: Horsehoof Ring - Patches", "Horsehoof Ring", missable=True, + npc=True, drop=True, shop=True), # (kill or buy) ], "Farron Keep": [ - DS3LocationData("FK: Ragged Mask", "Ragged Mask", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Iron Flesh", "Iron Flesh", DS3LocationCategory.SPELL), - DS3LocationData("FK: Golden Scroll", "Golden Scroll", DS3LocationCategory.MISC), - DS3LocationData("FK: Antiquated Dress", "Antiquated Dress", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Antiquated Gloves", "Antiquated Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Antiquated Skirt", "Antiquated Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Helm", "Nameless Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Armor", "Nameless Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Gauntlets", "Nameless Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Leggings", "Nameless Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Sunlight Talisman", "Sunlight Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Wolf's Blood Swordgrass", "Wolf's Blood Swordgrass", DS3LocationCategory.MISC), - DS3LocationData("FK: Greatsword", "Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Sage's Coal", "Sage's Coal", DS3LocationCategory.MISC), - DS3LocationData("FK: Stone Parma", "Stone Parma", DS3LocationCategory.SHIELD), - DS3LocationData("FK: Sage's Scroll", "Sage's Scroll", DS3LocationCategory.MISC), - DS3LocationData("FK: Crown of Dusk", "Crown of Dusk", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Lingering Dragoncrest Ring", "Lingering Dragoncrest Ring", DS3LocationCategory.RING), - DS3LocationData("FK: Pharis's Hat", "Pharis's Hat", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Black Bow of Pharis", "Black Bow of Pharis", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Dreamchaser's Ashes", "Dreamchaser's Ashes", DS3LocationCategory.MISC), - DS3LocationData("FK: Great Axe", "Great Axe", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Dragon Crest Shield", "Dragon Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("FK: Lightning Spear", "Lightning Spear", DS3LocationCategory.SPELL), - DS3LocationData("FK: Atonement", "Atonement", DS3LocationCategory.SPELL), - DS3LocationData("FK: Great Magic Weapon", "Great Magic Weapon", DS3LocationCategory.SPELL), - DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", "Cinders of a Lord - Abyss Watcher", DS3LocationCategory.KEY), - DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", DS3LocationCategory.BOSS), - DS3LocationData("FK: Soul of a Stray Demon", "Soul of a Stray Demon", DS3LocationCategory.BOSS), - DS3LocationData("FK: Watchdogs of Farron", "Watchdogs of Farron", DS3LocationCategory.MISC), + DS3LocationData("FK: Lightning Spear - upper keep, far side of the wall", + "Lightning Spear"), + DS3LocationData("FK: Dragon Crest Shield - upper keep, far side of the wall", + "Dragon Crest Shield"), + DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", + boss=True), + DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", + "Cinders of a Lord - Abyss Watcher", + static="03,0:50002100::", prominent=True, progression=True, + boss=True), + DS3LocationData("FK: Manikin Claws - Londor Pale Shade drop", "Manikin Claws", + missable=True, hostile_npc=True, + npc=True), # Londor Pale Shade (if Yoel/Yuria hostile) + DS3LocationData("FK: Purple Moss Clump - keep ruins, ritual island", + "Purple Moss Clump x2"), + DS3LocationData("FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfire", + "Purple Moss Clump x4"), + DS3LocationData("FK: Greatsword - ramp by keep ruins ritual island", "Greatsword"), + DS3LocationData("FK: Hollow Gem - perimeter, drop down into swamp", "Hollow Gem", + hidden=True), + DS3LocationData("FK: Purple Moss Clump - Farron Keep bonfire, around right corner", + "Purple Moss Clump x3"), + DS3LocationData("FK: Undead Bone Shard - pavilion by keep ruins bonfire island", + "Undead Bone Shard"), + DS3LocationData("FK: Atonement - perimeter, drop down into swamp", "Atonement", + hidden=True), + DS3LocationData("FK: Titanite Shard - by ladder to keep proper", "Titanite Shard"), + DS3LocationData("FK: Iron Flesh - Farron Keep bonfire, right after exit", "Iron Flesh"), + DS3LocationData("FK: Stone Parma - near wall by left island", "Stone Parma"), + DS3LocationData("FK: Rotten Pine Resin - left island, behind fire", "Rotten Pine Resin x2"), + DS3LocationData("FK: Titanite Shard - between left island and keep ruins", "Titanite Shard"), + DS3LocationData("FK: Rusted Gold Coin - right island, behind wall", "Rusted Gold Coin", + hidden=True), + DS3LocationData("FK: Nameless Knight Helm - corner of keep and right island", + "Nameless Knight Helm"), + DS3LocationData("FK: Nameless Knight Armor - corner of keep and right island", + "Nameless Knight Armor"), + DS3LocationData("FK: Nameless Knight Gauntlets - corner of keep and right island", + "Nameless Knight Gauntlets"), + DS3LocationData("FK: Nameless Knight Leggings - corner of keep and right island", + "Nameless Knight Leggings"), + DS3LocationData("FK: Shriving Stone - perimeter, just past stone doors", "Shriving Stone"), + DS3LocationData("FK: Repair Powder - outside hidden cave", "Repair Powder x4", + hidden=True), + DS3LocationData("FK: Golden Scroll - hidden cave", "Golden Scroll", hidden=True), + DS3LocationData("FK: Sage's Scroll - near wall by keep ruins bonfire island", + "Sage's Scroll"), + DS3LocationData("FK: Dreamchaser's Ashes - keep proper, illusory wall", + "Dreamchaser's Ashes", progression=True, hidden=True), + DS3LocationData("FK: Titanite Shard - keep ruins bonfire island, under ramp", + "Titanite Shard"), + DS3LocationData("FK: Wolf's Blood Swordgrass - by ladder to keep proper", + "Wolf's Blood Swordgrass"), + DS3LocationData("FK: Great Magic Weapon - perimeter, by door to Road of Sacrifices", + "Great Magic Weapon"), + DS3LocationData("FK: Ember - perimeter, path to boss", "Ember"), + DS3LocationData("FK: Titanite Shard - swamp by right island", "Titanite Shard x2"), + DS3LocationData("FK: Titanite Shard - by left island stairs", "Titanite Shard"), + DS3LocationData("FK: Titanite Shard - by keep ruins ritual island stairs", "Titanite Shard"), + DS3LocationData("FK: Black Bug Pellet - perimeter, hill by boss door", + "Black Bug Pellet x3"), + DS3LocationData("FK: Rotten Pine Resin - outside pavilion by left island", + "Rotten Pine Resin x4"), + DS3LocationData("FK: Poison Gem - near wall by keep ruins bridge", "Poison Gem"), + DS3LocationData("FK: Ragged Mask - Farron Keep bonfire, around left corner", "Ragged Mask"), + DS3LocationData("FK: Estus Shard - between Farron Keep bonfire and left island", + "Estus Shard"), + DS3LocationData("FK: Homeward Bone - right island, behind fire", "Homeward Bone x2"), + DS3LocationData("FK: Titanite Shard - Farron Keep bonfire, left after exit", + "Titanite Shard"), + DS3LocationData("FK: Large Soul of a Nameless Soldier - corner of keep and right island", + "Large Soul of a Nameless Soldier", hidden=True), # Tricky corner to spot + DS3LocationData("FK: Prism Stone - by left island stairs", "Prism Stone x10"), + DS3LocationData("FK: Large Soul of a Nameless Soldier - near wall by right island", + "Large Soul of a Nameless Soldier"), + DS3LocationData("FK: Sage's Coal - pavilion by left island", "Sage's Coal"), + DS3LocationData("FK: Gold Pine Bundle - by white tree", "Gold Pine Bundle x6"), + DS3LocationData("FK: Ember - by white tree", "Ember"), + DS3LocationData("FK: Soul of a Nameless Soldier - by white tree", "Soul of a Nameless Soldier"), + DS3LocationData("FK: Large Soul of an Unknown Traveler - by white tree", + "Large Soul of an Unknown Traveler"), + DS3LocationData("FK: Greataxe - upper keep, by miniboss", "Greataxe"), + DS3LocationData("FK: Ember - upper keep, by miniboss #1", "Ember"), + DS3LocationData("FK: Ember - upper keep, by miniboss #2", "Ember"), + DS3LocationData("FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wall", + "Dark Stoneplate Ring+2", ngp=True, hidden=True), + DS3LocationData("FK: Magic Stoneplate Ring+1 - between right island and wall", + "Magic Stoneplate Ring+1", ngp=True), + DS3LocationData("FK: Wolf Ring+1 - keep ruins bonfire island, outside building", + "Wolf Ring+1", ngp=True), + DS3LocationData("FK: Antiquated Dress - hidden cave", "Antiquated Dress", hidden=True), + DS3LocationData("FK: Antiquated Gloves - hidden cave", "Antiquated Gloves", hidden=True), + DS3LocationData("FK: Antiquated Skirt - hidden cave", "Antiquated Skirt", hidden=True), + DS3LocationData("FK: Sunlight Talisman - estus soup island, by ladder to keep proper", + "Sunlight Talisman"), + DS3LocationData("FK: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("FK: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("FK: Crown of Dusk - by white tree", "Crown of Dusk"), + DS3LocationData("FK: Lingering Dragoncrest Ring - by white tree, miniboss drop", + "Lingering Dragoncrest Ring", miniboss=True), # Great Crab drop + DS3LocationData("FK: Pharis's Hat - miniboss drop, by keep ruins near wall", + "Pharis's Hat", miniboss=True), # Elder Ghru drop + DS3LocationData("FK: Black Bow of Pharis - miniboss drop, by keep ruins near wall", + "Black Bow of Pharis", miniboss=True), # Elder Ghru drop + DS3LocationData("FK: Titanite Scale - perimeter, miniboss drop", "Titanite Scale x2", + miniboss=True), # Ravenous Crystal Lizard drop + DS3LocationData("FK: Large Titanite Shard - upper keep, lizard in open", + "Large Titanite Shard", lizard=True), + DS3LocationData("FK: Large Titanite Shard - upper keep, lizard by wyvern", + "Large Titanite Shard", lizard=True), + DS3LocationData("FK: Heavy Gem - upper keep, lizard on stairs", "Heavy Gem", lizard=True), + DS3LocationData("FK: Twinkling Titanite - keep proper, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("FK: Soul of a Stray Demon - upper keep, miniboss drop", + "Soul of a Stray Demon", miniboss=True), + DS3LocationData("FK: Watchdogs of Farron - Old Wolf", "Watchdogs of Farron"), + DS3LocationData("FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "Hawkwood's Shield", missable=True, + npc=True), # Hawkwood (quest, after Greatwood, Sage, Watchers, and Deacons) + DS3LocationData("US: Hawk Ring - Giant Archer", "Hawk Ring", drop=True, + npc=True), # Giant archer (kill or quest), here because you need to + # collect all seven White Branch locations to get it peacefully + + # Hawkwood after killing Abyss Watchers + DS3LocationData("FS: Farron Ring - Hawkwood", "Farron Ring", + missable=True, npc=True), + + # Shrine Handmaid after killing Abyss Watchers + DS3LocationData("FS: Undead Legion Helm - shop after killing FK boss", "Undead Legion Helm", + boss=True, shop=True), + DS3LocationData("FS: Undead Legion Armor - shop after killing FK boss", + "Undead Legion Armor", boss=True, shop=True), + DS3LocationData("FS: Undead Legion Gauntlet - shop after killing FK boss", + "Undead Legion Gauntlet", boss=True, shop=True), + DS3LocationData("FS: Undead Legion Leggings - shop after killing FK boss", + "Undead Legion Leggings", boss=True, shop=True), + + # Appears after killing Havel Knight in Archdragon Peak + DS3LocationData("FK: Havel's Helm - upper keep, after killing AP belfry roof NPC", + "Havel's Helm", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Armor - upper keep, after killing AP belfry roof NPC", + "Havel's Armor", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPC", + "Havel's Gauntlets", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Leggings - upper keep, after killing AP belfry roof NPC", + "Havel's Leggings", hidden=True, hostile_npc=True), ], "Catacombs of Carthus": [ - DS3LocationData("CC: Carthus Pyromancy Tome", "Carthus Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("CC: Carthus Milkring", "Carthus Milkring", DS3LocationCategory.RING), - DS3LocationData("CC: Grave Warden's Ashes", "Grave Warden's Ashes", DS3LocationCategory.MISC), - DS3LocationData("CC: Carthus Bloodring", "Carthus Bloodring", DS3LocationCategory.RING), - DS3LocationData("CC: Grave Warden Pyromancy Tome", "Grave Warden Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("CC: Old Sage's Blindfold", "Old Sage's Blindfold", DS3LocationCategory.ARMOR), - DS3LocationData("CC: Witch's Ring", "Witch's Ring", DS3LocationCategory.RING), - DS3LocationData("CC: Black Blade", "Black Blade", DS3LocationCategory.WEAPON), - DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", DS3LocationCategory.BOSS), - DS3LocationData("CC: Soul of a Demon", "Soul of a Demon", DS3LocationCategory.BOSS), + DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", + prominent=True, boss=True), + DS3LocationData("CC: Carthus Rouge - atrium upper, left after entrance", + "Carthus Rouge x2"), + DS3LocationData("CC: Sharp Gem - atrium lower, right before exit", "Sharp Gem"), + DS3LocationData("CC: Soul of a Nameless Soldier - atrium lower, down hall", + "Soul of a Nameless Soldier"), + DS3LocationData("CC: Titanite Shard - atrium lower, corner by stairs", "Titanite Shard x2"), + DS3LocationData("CC: Bloodred Moss Clump - atrium lower, down more stairs", + "Bloodred Moss Clump x3"), + DS3LocationData("CC: Carthus Milkring - crypt upper, among pots", "Carthus Milkring"), + DS3LocationData("CC: Ember - atrium, on long stairway", "Ember"), + DS3LocationData("CC: Carthus Rouge - crypt across, corner", "Carthus Rouge x3"), + DS3LocationData("CC: Ember - crypt upper, end of hall past hole", "Ember"), + DS3LocationData("CC: Carthus Bloodring - crypt lower, end of side hall", "Carthus Bloodring"), + DS3LocationData("CC: Titanite Shard - crypt lower, left of entrance", "Titanite Shard x2"), + DS3LocationData("CC: Titanite Shard - crypt lower, start of side hall", "Titanite Shard x2"), + DS3LocationData("CC: Ember - crypt lower, shortcut to cavern", "Ember"), + DS3LocationData("CC: Carthus Pyromancy Tome - atrium lower, jump from bridge", + "Carthus Pyromancy Tome", + hidden=True), # Behind illusory wall or hidden drop + DS3LocationData("CC: Large Titanite Shard - crypt upper, skeleton ball hall", + "Large Titanite Shard"), + DS3LocationData("CC: Large Titanite Shard - crypt across, middle hall", + "Large Titanite Shard"), + DS3LocationData("CC: Yellow Bug Pellet - cavern, on overlook", "Yellow Bug Pellet x3"), + DS3LocationData("CC: Large Soul of a Nameless Soldier - cavern, before bridge", + "Large Soul of a Nameless Soldier"), + DS3LocationData("CC: Black Bug Pellet - cavern, before bridge", "Black Bug Pellet x2"), + DS3LocationData("CC: Grave Warden's Ashes - crypt across, corner", "Grave Warden's Ashes", + progression=True), + DS3LocationData("CC: Large Titanite Shard - tomb lower", "Large Titanite Shard"), + DS3LocationData("CC: Large Soul of a Nameless Soldier - tomb lower", + "Large Soul of a Nameless Soldier"), + DS3LocationData("CC: Old Sage's Blindfold - tomb, hall before bonfire", + "Old Sage's Blindfold"), + DS3LocationData("CC: Witch's Ring - tomb, hall before bonfire", "Witch's Ring"), + DS3LocationData("CC: Soul of a Nameless Soldier - atrium upper, up more stairs", + "Soul of a Nameless Soldier"), + DS3LocationData("CC: Grave Warden Pyromancy Tome - boss arena", + "Grave Warden Pyromancy Tome"), + DS3LocationData("CC: Large Soul of an Unknown Traveler - crypt upper, hall middle", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CC: Ring of Steel Protection+2 - atrium upper, drop onto pillar", + "Ring of Steel Protection+2", ngp=True), + DS3LocationData("CC: Thunder Stoneplate Ring+1 - crypt upper, among pots", + "Thunder Stoneplate Ring+1", ngp=True), + DS3LocationData("CC: Undead Bone Shard - crypt upper, skeleton ball drop", + "Undead Bone Shard", hidden=True), # Skeleton Ball puzzle + DS3LocationData("CC: Dark Gem - crypt lower, skeleton ball drop", "Dark Gem", + hidden=True), # Skeleton Ball puzzle + DS3LocationData("CC: Black Blade - tomb, mimic", "Black Blade", mimic=True), + DS3LocationData("CC: Soul of a Demon - tomb, miniboss drop", "Soul of a Demon", + miniboss=True), + DS3LocationData("CC: Twinkling Titanite - atrium lower, lizard down more stairs", + "Twinkling Titanite", lizard=True), + DS3LocationData("CC: Fire Gem - cavern, lizard", "Fire Gem", lizard=True), + DS3LocationData("CC: Homeward Bone - Irithyll bridge", "Homeward Bone"), + DS3LocationData("CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop", + "Pontiff's Right Eye", miniboss=True), # Sulyvahn's Beast drop + + # Shrine Handmaid after killing High Lord Wolnir + DS3LocationData("FS: Wolnir's Crown - shop after killing CC boss", "Wolnir's Crown", + boss=True, shop=True), ], "Smouldering Lake": [ - DS3LocationData("SL: Shield of Want", "Shield of Want", DS3LocationCategory.SHIELD), - DS3LocationData("SL: Speckled Stoneplate Ring", "Speckled Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("SL: Dragonrider Bow", "Dragonrider Bow", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Lightning Stake", "Lightning Stake", DS3LocationCategory.SPELL), - DS3LocationData("SL: Izalith Pyromancy Tome", "Izalith Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("SL: Black Knight Sword", "Black Knight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Quelana Pyromancy Tome", "Quelana Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("SL: Toxic Mist", "Toxic Mist", DS3LocationCategory.SPELL), - DS3LocationData("SL: White Hair Talisman", "White Hair Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Izalith Staff", "Izalith Staff", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Sacred Flame", "Sacred Flame", DS3LocationCategory.SPELL), - DS3LocationData("SL: Fume Ultra Greatsword", "Fume Ultra Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Black Iron Greatshield", "Black Iron Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", DS3LocationCategory.BOSS), - DS3LocationData("SL: Knight Slayer's Ring", "Knight Slayer's Ring", DS3LocationCategory.RING), + DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", + prominent=True, boss=True), + DS3LocationData("SL: Fume Ultra Greatsword - ruins basement, NPC drop", + "Fume Ultra Greatsword", hostile_npc=True), # Knight Slayer Tsorig drop + DS3LocationData("SL: Black Iron Greatshield - ruins basement, NPC drop", + "Black Iron Greatshield", hostile_npc=True), # Knight Slayer Tsorig drop + DS3LocationData("SL: Large Titanite Shard - ledge by Demon Ruins bonfire", + "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by entrance", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, straight from entrance", + "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by tree #1", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by miniboss", "Large Titanite Shard"), + DS3LocationData("SL: Yellow Bug Pellet - side lake", "Yellow Bug Pellet x2"), + DS3LocationData("SL: Large Titanite Shard - side lake #1", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - side lake #2", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by tree #2", "Large Titanite Shard"), + DS3LocationData("SL: Speckled Stoneplate Ring - lake, ballista breaks bricks", + "Speckled Stoneplate Ring", hidden=True), # Requires careful ballista shot + DS3LocationData("SL: Homeward Bone - path to ballista", "Homeward Bone x2"), + DS3LocationData("SL: Ember - ruins main upper, hall end by hole", "Ember"), + DS3LocationData("SL: Chaos Gem - lake, far end by mob", "Chaos Gem"), + DS3LocationData("SL: Ember - ruins main lower, path to antechamber", "Ember"), + DS3LocationData("SL: Izalith Pyromancy Tome - antechamber, room near bonfire", + "Izalith Pyromancy Tome"), + DS3LocationData("SL: Black Knight Sword - ruins main lower, illusory wall in far hall", + "Black Knight Sword", hidden=True), + DS3LocationData("SL: Ember - ruins main upper, just after entrance", "Ember"), + DS3LocationData("SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey room", + "Quelana Pyromancy Tome", hidden=True), + DS3LocationData("SL: Izalith Staff - ruins basement, second illusory wall behind chest", + "Izalith Staff", hidden=True), + DS3LocationData("SL: White Hair Talisman - ruins main lower, in lava", + "White Hair Talisman", + missable=True), # This may not even be possible to get without enough fire + # protection gear which the player may not have + DS3LocationData("SL: Toxic Mist - ruins main lower, in lava", "Toxic Mist", + missable=True), # This is _probably_ reachable with normal gear, but it + # still sucks and will probably force a death. + DS3LocationData("SL: Undead Bone Shard - ruins main lower, left after stairs", + "Undead Bone Shard"), + DS3LocationData("SL: Titanite Scale - ruins basement, path to lava", "Titanite Scale"), + DS3LocationData("SL: Shield of Want - lake, by miniboss", "Shield of Want"), + DS3LocationData("SL: Soul of a Crestfallen Knight - ruins basement, above lava", + "Soul of a Crestfallen Knight"), + + # Lava items are missable because they require a complex set of armor, rings, spells, and + # undead bone shards to reliably access without dying. + DS3LocationData("SL: Ember - ruins basement, in lava", "Ember", missable=True), # In lava + DS3LocationData("SL: Sacred Flame - ruins basement, in lava", "Sacred Flame", + missable=True), # In lava + + DS3LocationData("SL: Dragonrider Bow - by ladder from ruins basement to ballista", + "Dragonrider Bow", hidden=True), # Hidden fall + DS3LocationData("SL: Estus Shard - antechamber, illusory wall", "Estus Shard", + hidden=True), + DS3LocationData("SL: Bloodbite Ring+1 - behind ballista", "Bloodbite Ring+1", ngp=True), + DS3LocationData("SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hall", + "Flame Stoneplate Ring+2", ngp=True, hidden=True), + DS3LocationData("SL: Large Titanite Shard - ruins basement, illusory wall in upper hall", + "Large Titanite Shard x3", hidden=True), + DS3LocationData("SL: Undead Bone Shard - lake, miniboss drop", "Undead Bone Shard", + miniboss=True), # Sand Worm drop + DS3LocationData("SL: Lightning Stake - lake, miniboss drop", "Lightning Stake", + miniboss=True), # Sand Worm drop + DS3LocationData("SL: Twinkling Titanite - path to side lake, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("SL: Titanite Chunk - path to side lake, lizard", "Titanite Chunk", + lizard=True), + DS3LocationData("SL: Chaos Gem - antechamber, lizard at end of long hall", "Chaos Gem", + lizard=True), + DS3LocationData("SL: Knight Slayer's Ring - ruins basement, NPC drop", + "Knight Slayer's Ring", hostile_npc=True), # Knight Slayer Tsorig drop + + # Horace the Hushed + # These are listed here even though you can kill Horace in the Road of Sacrifices because + # the player may want to complete his and Anri's quest first. + DS3LocationData("SL: Llewellyn Shield - Horace drop", "Llewellyn Shield", npc=True, + hostile_npc=True), + DS3LocationData("FS: Executioner Helm - shop after killing Horace", "Executioner Helm", + npc=True, hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Executioner Armor - shop after killing Horace", "Executioner Armor", + npc=True, hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Executioner Gauntlets - shop after killing Horace", + "Executioner Gauntlets", hostile_npc=True, npc=True, shop=True, + hidden=True), + DS3LocationData("FS: Executioner Leggings - shop after killing Horace", + "Executioner Leggings", hostile_npc=True, npc=True, shop=True, + hidden=True), + + # Shrine Handmaid after killing Knight Slayer Tsorig + DS3LocationData("FS: Black Iron Helm - shop after killing Tsorig", "Black Iron Helm", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Armor - shop after killing Tsorig", "Black Iron Armor", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Gauntlets - shop after killing Tsorig", + "Black Iron Gauntlets", hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Leggings - shop after killing Tsorig", + "Black Iron Leggings", hostile_npc=True, shop=True, hidden=True), + + # Near Cornyx's cage after killing Old Demon King with Cuculus + DS3LocationData("US: Spotted Whip - by Cornyx's cage after Cuculus quest", "Spotted Whip", + missable=True, boss=True, npc=True), + DS3LocationData("US: Cornyx's Garb - by Cornyx's cage after Cuculus quest", + "Cornyx's Garb", static='02,0:53100100::', missable=True, boss=True, + npc=True), + DS3LocationData("US: Cornyx's Wrap - by Cornyx's cage after Cuculus quest", "Cornyx's Wrap", + static='02,0:53100100::', missable=True, boss=True, npc=True), + DS3LocationData("US: Cornyx's Skirt - by Cornyx's cage after Cuculus quest", + "Cornyx's Skirt", static='02,0:53100100::', missable=True, boss=True, + npc=True), ], "Irithyll of the Boreal Valley": [ - DS3LocationData("IBV: Dorhys' Gnawing", "Dorhys' Gnawing", DS3LocationCategory.SPELL), - DS3LocationData("IBV: Witchtree Branch", "Witchtree Branch", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Magic Clutch Ring", "Magic Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Ring of the Sun's First Born", "Ring of the Sun's First Born", DS3LocationCategory.RING), - DS3LocationData("IBV: Roster of Knights", "Roster of Knights", DS3LocationCategory.MISC), - DS3LocationData("IBV: Pontiff's Right Eye", "Pontiff's Right Eye", DS3LocationCategory.RING), - DS3LocationData("IBV: Yorshka's Spear", "Yorshka's Spear", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Great Heal", "Great Heal", DS3LocationCategory.SPELL), - DS3LocationData("IBV: Smough's Great Hammer", "Smough's Great Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Leo Ring", "Leo Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Excrement-covered Ashes", "Excrement-covered Ashes", DS3LocationCategory.MISC), - DS3LocationData("IBV: Dark Stoneplate Ring", "Dark Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Easterner's Ashes", "Easterner's Ashes", DS3LocationCategory.MISC), - DS3LocationData("IBV: Painting Guardian's Curved Sword", "Painting Guardian's Curved Sword", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Painting Guardian Hood", "Painting Guardian Hood", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Gown", "Painting Guardian Gown", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Gloves", "Painting Guardian Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Waistcloth", "Painting Guardian Waistcloth", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Dragonslayer Greatbow", "Dragonslayer Greatbow", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Reversal Ring", "Reversal Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Brass Helm", "Brass Helm", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Armor", "Brass Armor", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Gauntlets", "Brass Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Leggings", "Brass Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Ring of Favor", "Ring of Favor", DS3LocationCategory.RING), - DS3LocationData("IBV: Golden Ritual Spear", "Golden Ritual Spear", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", DS3LocationCategory.BOSS), - DS3LocationData("IBV: Aldrich Faithful", "Aldrich Faithful", DS3LocationCategory.MISC), - DS3LocationData("IBV: Drang Twinspears", "Drang Twinspears", DS3LocationCategory.WEAPON), + DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", + prominent=True, boss=True), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - central, by bonfire", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Large Titanite Shard - ascent, down ladder in last building", + "Large Titanite Shard"), + DS3LocationData("IBV: Soul of a Weary Warrior - central, by first fountain", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Soul of a Weary Warrior - central, railing by first fountain", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Rime-blue Moss Clump - central, by bonfire", "Rime-blue Moss Clump"), + DS3LocationData("IBV: Witchtree Branch - by Dorhys", "Witchtree Branch", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Large Titanite Shard - central, side path after first fountain", + "Large Titanite Shard"), + DS3LocationData("IBV: Budding Green Blossom - central, by second fountain", + "Budding Green Blossom"), + DS3LocationData("IBV: Rime-blue Moss Clump - central, past second fountain", + "Rime-blue Moss Clump x2"), + DS3LocationData("IBV: Large Titanite Shard - central, balcony just before plaza", + "Large Titanite Shard"), + DS3LocationData("IBV: Large Titanite Shard - path to Dorhys", "Large Titanite Shard", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Ring of the Sun's First Born - fall from in front of cathedral", + "Ring of the Sun's First Born", + hidden=True), # Hidden fall + DS3LocationData("IBV: Large Soul of a Nameless Soldier - path to plaza", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Large Titanite Shard - plaza, balcony overlooking ascent", + "Large Titanite Shard"), + DS3LocationData("IBV: Large Titanite Shard - plaza, by stairs to church", + "Large Titanite Shard"), + DS3LocationData("IBV: Soul of a Weary Warrior - plaza, side room lower", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Magic Clutch Ring - plaza, illusory wall", "Magic Clutch Ring", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Fading Soul - descent, cliff edge #1", "Fading Soul"), + DS3LocationData("IBV: Fading Soul - descent, cliff edge #2", "Fading Soul"), + DS3LocationData("IBV: Homeward Bone - descent, before gravestone", "Homeward Bone x3"), + DS3LocationData("IBV: Undead Bone Shard - descent, behind gravestone", "Undead Bone Shard", + hidden=True), # Hidden behind gravestone + DS3LocationData("IBV: Kukri - descent, side path", "Kukri x8"), + DS3LocationData("IBV: Rusted Gold Coin - descent, side path", "Rusted Gold Coin"), + DS3LocationData("IBV: Blue Bug Pellet - descent, dark room", "Blue Bug Pellet x2"), + DS3LocationData("IBV: Shriving Stone - descent, dark room rafters", "Shriving Stone"), + DS3LocationData("IBV: Blood Gem - descent, platform before lake", "Blood Gem"), + DS3LocationData("IBV: Green Blossom - lake, by stairs from descent", "Green Blossom x3"), + DS3LocationData("IBV: Ring of Sacrifice - lake, right of stairs from descent", + "Ring of Sacrifice"), + DS3LocationData("IBV: Great Heal - lake, dead Corpse-Grub", "Great Heal"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - lake island", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Green Blossom - lake wall", "Green Blossom x3"), + DS3LocationData("IBV: Dung Pie - sewer #1", "Dung Pie x3"), + DS3LocationData("IBV: Dung Pie - sewer #2", "Dung Pie x3"), + # These don't actually guard any single item sales. Maybe we can inject one manually? + DS3LocationData("IBV: Excrement-covered Ashes - sewer, by stairs", + "Excrement-covered Ashes"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - ascent, after great hall", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Soul of a Weary Warrior - ascent, by final staircase", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Large Titanite Shard - ascent, by elevator door", + "Large Titanite Shard"), + DS3LocationData("IBV: Blue Bug Pellet - ascent, in last building", "Blue Bug Pellet x2"), + DS3LocationData("IBV: Ember - shortcut from church to cathedral", "Ember"), + DS3LocationData("IBV: Green Blossom - lake, by Distant Manor", "Green Blossom"), + DS3LocationData("IBV: Lightning Gem - plaza center", "Lightning Gem"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - central, by second fountain", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Soul of a Weary Warrior - plaza, side room upper", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Proof of a Concord Kept - Church of Yorshka altar", + "Proof of a Concord Kept"), + DS3LocationData("IBV: Rusted Gold Coin - Distant Manor, drop after stairs", + "Rusted Gold Coin"), + DS3LocationData("IBV: Chloranthy Ring+1 - plaza, behind altar", "Chloranthy Ring+1", + ngp=True), + DS3LocationData("IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark room", + "Covetous Gold Serpent Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("IBV: Wood Grain Ring+2 - ascent, right after great hall", "Wood Grain Ring+2", + ngp=True), + DS3LocationData("IBV: Divine Blessing - great hall, chest", "Divine Blessing"), + DS3LocationData("IBV: Smough's Great Hammer - great hall, chest", + "Smough's Great Hammer"), + DS3LocationData("IBV: Yorshka's Spear - descent, dark room rafters chest", "Yorshka's Spear"), + DS3LocationData("IBV: Leo Ring - great hall, chest", "Leo Ring"), + DS3LocationData("IBV: Dorhys' Gnawing - Dorhys drop", "Dorhys' Gnawing", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Divine Blessing - great hall, mob drop", + "Divine Blessing", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, main floor mob drop", + "Large Titanite Shard", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, upstairs mob drop #1", + "Large Titanite Shard x2", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, upstairs mob drop #2", + "Large Titanite Shard x2", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Roster of Knights - descent, first landing", "Roster of Knights"), + DS3LocationData("IBV: Twinkling Titanite - descent, lizard behind illusory wall", + "Twinkling Titanite", lizard=True, hidden=True), # Behind illusory wall + DS3LocationData("IBV: Twinkling Titanite - central, lizard before plaza", + "Twinkling Titanite", lizard=True), + DS3LocationData("IBV: Large Titanite Shard - Distant Manor, under overhang", + "Large Titanite Shard"), + DS3LocationData("IBV: Siegbräu - Siegward", "Siegbräu", missable=True, npc=True), + DS3LocationData("IBV: Emit Force - Siegward", "Emit Force", missable=True, npc=True), + DS3LocationData("IBV -> ID", None), + + # After winning both Londor Pale Shade invasions + DS3LocationData("FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twice", + "Sneering Mask", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Robe", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Gloves", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Trousers", missable=True, hostile_npc=True), + + # Anri of Astora + DS3LocationData("IBV: Ring of the Evil Eye - Anri", "Ring of the Evil Eye", missable=True, + npc=True), + + # Sirris quest after killing Creighton + DS3LocationData("FS: Mail Breaker - Sirris for killing Creighton", "Mail Breaker", + static='99,0:50006080::', missable=True, hostile_npc=True, + npc=True), + DS3LocationData("FS: Silvercat Ring - Sirris for killing Creighton", "Silvercat Ring", + missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Dragonslayer's Axe - Creighton drop", "Dragonslayer's Axe", + missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Creighton's Steel Mask - bridge after killing Creighton", + "Creighton's Steel Mask", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Mail - bridge after killing Creighton", + "Mirrah Chain Mail", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Gloves - bridge after killing Creighton", + "Mirrah Chain Gloves", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Leggings - bridge after killing Creighton", + "Mirrah Chain Leggings", missable=True, hostile_npc=True, npc=True), ], "Irithyll Dungeon": [ - DS3LocationData("ID: Bellowing Dragoncrest Ring", "Bellowing Dragoncrest Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Jailbreaker's Key", "Jailbreaker's Key", DS3LocationCategory.KEY), - DS3LocationData("ID: Prisoner Chief's Ashes", "Prisoner Chief's Ashes", DS3LocationCategory.KEY), - DS3LocationData("ID: Old Sorcerer Hat", "Old Sorcerer Hat", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Coat", "Old Sorcerer Coat", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Gauntlets", "Old Sorcerer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Boots", "Old Sorcerer Boots", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Great Magic Shield", "Great Magic Shield", DS3LocationCategory.SPELL), - DS3LocationData("ID: Dragon Torso Stone", "Dragon Torso Stone", DS3LocationCategory.MISC), - DS3LocationData("ID: Lightning Blade", "Lightning Blade", DS3LocationCategory.SPELL), - DS3LocationData("ID: Profaned Coal", "Profaned Coal", DS3LocationCategory.MISC), - DS3LocationData("ID: Xanthous Ashes", "Xanthous Ashes", DS3LocationCategory.MISC), - DS3LocationData("ID: Old Cell Key", "Old Cell Key", DS3LocationCategory.KEY), - DS3LocationData("ID: Pickaxe", "Pickaxe", DS3LocationCategory.WEAPON), - DS3LocationData("ID: Profaned Flame", "Profaned Flame", DS3LocationCategory.SPELL), - DS3LocationData("ID: Covetous Gold Serpent Ring", "Covetous Gold Serpent Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Jailer's Key Ring", "Jailer's Key Ring", DS3LocationCategory.KEY), - DS3LocationData("ID: Dusk Crown Ring", "Dusk Crown Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Dark Clutch Ring", "Dark Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Karla's Ashes", "Karla's Ashes", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Pointed Hat", "Karla's Pointed Hat", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Coat", "Karla's Coat", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Gloves", "Karla's Gloves", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Trousers", "Karla's Trousers", DS3LocationCategory.NPC), + DS3LocationData("ID: Titanite Slab - Siegward", "Titanite Slab", missable=True, + npc=True), + DS3LocationData("ID: Murakumo - Alva drop", "Murakumo", missable=True, + hostile_npc=True), + DS3LocationData("ID: Large Titanite Shard - after bonfire, second cell on left", + "Large Titanite Shard"), + DS3LocationData("ID: Fading Soul - B1 near, main hall", "Fading Soul"), + DS3LocationData("ID: Large Soul of a Nameless Soldier - B2, hall by stairs", + "Large Soul of a Nameless Soldier"), + DS3LocationData("ID: Jailbreaker's Key - B1 far, cell after gate", "Jailbreaker's Key"), + DS3LocationData("ID: Pale Pine Resin - B1 far, cell with broken wall", + "Pale Pine Resin x2"), + DS3LocationData("ID: Simple Gem - B2 far, cell by stairs", "Simple Gem"), + DS3LocationData("ID: Large Soul of a Nameless Soldier - B2 far, by lift", + "Large Soul of a Nameless Soldier"), + DS3LocationData("ID: Large Titanite Shard - B1 far, rightmost cell", + "Large Titanite Shard"), + DS3LocationData("ID: Homeward Bone - path from B2 to pit", "Homeward Bone x2"), + DS3LocationData("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", + "Bellowing Dragoncrest Ring", conditional=True), + DS3LocationData("ID: Soul of a Weary Warrior - by drop to pit", "Soul of a Weary Warrior"), + DS3LocationData("ID: Soul of a Crestfallen Knight - balcony above pit", + "Soul of a Crestfallen Knight"), + DS3LocationData("ID: Lightning Bolt - awning over pit", "Lightning Bolt x9"), + DS3LocationData("ID: Large Titanite Shard - pit #1", "Large Titanite Shard"), + DS3LocationData("ID: Profaned Flame - pit", "Profaned Flame"), + DS3LocationData("ID: Large Titanite Shard - pit #2", "Large Titanite Shard"), + DS3LocationData("ID: Soul of a Weary Warrior - stairs between pit and B3", + "Soul of a Weary Warrior"), + DS3LocationData("ID: Dung Pie - B3, by path from pit", "Dung Pie x4"), + DS3LocationData("ID: Ember - B3 center", "Ember"), + DS3LocationData("ID: Ember - B3 far right", "Ember"), + DS3LocationData("ID: Profaned Coal - B3 far, left cell", "Profaned Coal"), + DS3LocationData("ID: Large Titanite Shard - B3 near, right corner", "Large Titanite Shard"), + DS3LocationData("ID: Old Sorcerer Hat - B2 near, middle cell", "Old Sorcerer Hat"), + DS3LocationData("ID: Old Sorcerer Coat - B2 near, middle cell", "Old Sorcerer Coat"), + DS3LocationData("ID: Old Sorcerer Gauntlets - B2 near, middle cell", + "Old Sorcerer Gauntlets"), + DS3LocationData("ID: Old Sorcerer Boots - B2 near, middle cell", "Old Sorcerer Boots"), + DS3LocationData("ID: Large Soul of a Weary Warrior - just before Profaned Capital", + "Large Soul of a Weary Warrior"), + DS3LocationData("ID: Covetous Gold Serpent Ring - Siegward's cell", + "Covetous Gold Serpent Ring", conditional=True), + DS3LocationData("ID: Lightning Blade - B3 lift, middle platform", "Lightning Blade"), + DS3LocationData("ID: Rusted Coin - after bonfire, first cell on left", "Rusted Coin"), + DS3LocationData("ID: Dusk Crown Ring - B3 far, right cell", "Dusk Crown Ring"), + DS3LocationData("ID: Pickaxe - path from pit to B3", "Pickaxe"), + DS3LocationData("ID: Xanthous Ashes - B3 far, right cell", "Xanthous Ashes", + progression=True), + DS3LocationData("ID: Large Titanite Shard - B1 near, by door", "Large Titanite Shard"), + DS3LocationData("ID: Rusted Gold Coin - after bonfire, last cell on right", + "Rusted Gold Coin"), + DS3LocationData("ID: Old Cell Key - stairs between pit and B3", "Old Cell Key"), + DS3LocationData("ID: Covetous Silver Serpent Ring+1 - pit lift, middle platform", + "Covetous Silver Serpent Ring+1", ngp=True), + DS3LocationData("ID: Dragon Torso Stone - B3, outside lift", "Dragon Torso Stone"), + DS3LocationData("ID: Prisoner Chief's Ashes - B2 near, locked cell by stairs", + "Prisoner Chief's Ashes", progression=True), + DS3LocationData("ID: Great Magic Shield - B2 near, mob drop in far left cell", + "Great Magic Shield", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Corpse-Grub + DS3LocationData("ID: Dragonslayer Lightning Arrow - pit, mimic in hall", + "Dragonslayer Lightning Arrow x10", mimic=True), + DS3LocationData("ID: Titanite Scale - B3 far, mimic in hall", "Titanite Scale x2", + mimic=True), + DS3LocationData("ID: Dark Clutch Ring - stairs between pit and B3, mimic", + "Dark Clutch Ring", mimic=True), + DS3LocationData("ID: Estus Shard - mimic on path from B2 to pit", "Estus Shard", + mimic=True), + DS3LocationData("ID: Titanite Chunk - balcony above pit, lizard", "Titanite Chunk", + lizard=True), + DS3LocationData("ID: Titanite Scale - B2 far, lizard", "Titanite Scale", lizard=True), + + # These are missable because of a bug that causes them to be dropped wherever the giant is + # randomized to, instead of where the miniboss is in vanilla. + DS3LocationData("ID: Dung Pie - pit, miniboss drop", "Dung Pie x4", + miniboss=True, missable=True), # Giant slave drop + DS3LocationData("ID: Titanite Chunk - pit, miniboss drop", "Titanite Chunk", + miniboss=True, missable=True), # Giant Slave Drop + + # Alva (requires ember) + DS3LocationData("ID: Alva Helm - B3 near, by Karla's cell, after killing Alva", "Alva Helm", + missable=True, npc=True), + DS3LocationData("ID: Alva Armor - B3 near, by Karla's cell, after killing Alva", + "Alva Armor", missable=True, npc=True), + DS3LocationData("ID: Alva Gauntlets - B3 near, by Karla's cell, after killing Alva", + "Alva Gauntlets", missable=True, npc=True), + DS3LocationData("ID: Alva Leggings - B3 near, by Karla's cell, after killing Alva", + "Alva Leggings", missable=True, npc=True), ], "Profaned Capital": [ - DS3LocationData("PC: Cursebite Ring", "Cursebite Ring", DS3LocationCategory.RING), - DS3LocationData("PC: Court Sorcerer Hood", "Court Sorcerer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Robe", "Court Sorcerer Robe", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Gloves", "Court Sorcerer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Trousers", "Court Sorcerer Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Wrath of the Gods", "Wrath of the Gods", DS3LocationCategory.SPELL), - DS3LocationData("PC: Logan's Scroll", "Logan's Scroll", DS3LocationCategory.MISC), - DS3LocationData("PC: Eleonora", "Eleonora", DS3LocationCategory.WEAPON), - DS3LocationData("PC: Court Sorcerer's Staff", "Court Sorcerer's Staff", DS3LocationCategory.WEAPON), - DS3LocationData("PC: Greatshield of Glory", "Greatshield of Glory", DS3LocationCategory.SHIELD), - DS3LocationData("PC: Storm Ruler", "Storm Ruler", DS3LocationCategory.KEY), - DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", "Cinders of a Lord - Yhorm the Giant", DS3LocationCategory.KEY), - DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", DS3LocationCategory.BOSS), + DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", boss=True), + DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", + "Cinders of a Lord - Yhorm the Giant", static="07,0:50002170::", + prominent=True, progression=True, boss=True), + DS3LocationData("PC: Logan's Scroll - chapel roof, NPC drop", "Logan's Scroll", + hostile_npc=True), # Sorcerer + DS3LocationData("PC: Purging Stone - chapel ground floor", "Purging Stone x3"), + DS3LocationData("PC: Rusted Coin - tower exterior", "Rusted Coin x2"), + DS3LocationData("PC: Rusted Gold Coin - halls above swamp", "Rusted Gold Coin"), + DS3LocationData("PC: Purging Stone - swamp, by chapel ladder", "Purging Stone"), + DS3LocationData("PC: Cursebite Ring - swamp, below halls", "Cursebite Ring"), + DS3LocationData("PC: Poison Gem - swamp, below halls", "Poison Gem"), + DS3LocationData("PC: Shriving Stone - swamp, by chapel door", "Shriving Stone"), + DS3LocationData("PC: Poison Arrow - chapel roof", "Poison Arrow x18"), + DS3LocationData("PC: Rubbish - chapel, down stairs from second floor", "Rubbish"), + DS3LocationData("PC: Onislayer Greatarrow - bridge", "Onislayer Greatarrow x8"), + DS3LocationData("PC: Large Soul of a Weary Warrior - bridge, far end", + "Large Soul of a Weary Warrior"), + DS3LocationData("PC: Rusted Coin - below bridge #1", "Rusted Coin"), + DS3LocationData("PC: Rusted Coin - below bridge #2", "Rusted Coin"), + DS3LocationData("PC: Blooming Purple Moss Clump - walkway above swamp", + "Blooming Purple Moss Clump x3"), + DS3LocationData("PC: Wrath of the Gods - chapel, drop from roof", "Wrath of the Gods"), + DS3LocationData("PC: Onislayer Greatbow - drop from bridge", "Onislayer Greatbow", + hidden=True), # Hidden fall + DS3LocationData("PC: Jailer's Key Ring - hall past chapel", "Jailer's Key Ring", + progression=True), + DS3LocationData("PC: Ember - palace, far room", "Ember"), + DS3LocationData("PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entrance", + "Flame Stoneplate Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("PC: Magic Stoneplate Ring+2 - tower base", "Magic Stoneplate Ring+2", + ngp=True), + DS3LocationData("PC: Court Sorcerer Hood - chapel, second floor", "Court Sorcerer Hood"), + DS3LocationData("PC: Court Sorcerer Robe - chapel, second floor", "Court Sorcerer Robe"), + DS3LocationData("PC: Court Sorcerer Gloves - chapel, second floor", "Court Sorcerer Gloves"), + DS3LocationData("PC: Court Sorcerer Trousers - chapel, second floor", + "Court Sorcerer Trousers"), + DS3LocationData("PC: Storm Ruler - boss room", "Storm Ruler"), + DS3LocationData("PC: Undead Bone Shard - by bonfire", "Undead Bone Shard"), + DS3LocationData("PC: Eleonora - chapel ground floor, kill mob", "Eleonora", + drop=True, + hidden=True), # Guaranteed drop from a normal-looking Monstrosity of Sin + DS3LocationData("PC: Rusted Gold Coin - palace, mimic in far room", "Rusted Gold Coin x2", + mimic=True), + DS3LocationData("PC: Court Sorcerer's Staff - chapel, mimic on second floor", + "Court Sorcerer's Staff", mimic=True), + DS3LocationData("PC: Greatshield of Glory - palace, mimic in far room", + "Greatshield of Glory", mimic=True), + DS3LocationData("PC: Twinkling Titanite - halls above swamp, lizard #1", + "Twinkling Titanite", lizard=True), + DS3LocationData("PC: Twinkling Titanite - halls above swamp, lizard #2", + "Twinkling Titanite", lizard=True), + DS3LocationData("PC: Siegbräu - Siegward after killing boss", "Siegbräu", + missable=True, npc=True), + + # Siegward drops (kill or quest) + DS3LocationData("PC: Storm Ruler - Siegward", "Storm Ruler", static='02,0:50006218::', + missable=True, drop=True, npc=True), + DS3LocationData("PC: Pierce Shield - Siegward", "Pierce Shield", missable=True, + drop=True, npc=True), ], + # We consider "Anor Londo" to be everything accessible only after killing Pontiff. This doesn't + # match up one-to-one with where the game pops up the region name, but it balances items better + # and covers the region that's full of DS1 Anor Londo references. "Anor Londo": [ - DS3LocationData("AL: Giant's Coal", "Giant's Coal", DS3LocationCategory.MISC), - DS3LocationData("AL: Sun Princess Ring", "Sun Princess Ring", DS3LocationCategory.RING), - DS3LocationData("AL: Aldrich's Ruby", "Aldrich's Ruby", DS3LocationCategory.RING), - DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", DS3LocationCategory.KEY), - DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", DS3LocationCategory.BOSS), + DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", boss=True), + DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", + static='06,0:50002130::', prominent=True, progression=True, + boss=True), + DS3LocationData("AL: Yorshka's Chime - kill Yorshka", "Yorshka's Chime", missable=True, + drop=True, + npc=True), # Hidden walkway, missable because it will break Sirris's quest + DS3LocationData("AL: Drang Twinspears - plaza, NPC drop", "Drang Twinspears", drop=True, + hidden=True), + DS3LocationData("AL: Estus Shard - dark cathedral, by left stairs", "Estus Shard"), + DS3LocationData("AL: Painting Guardian's Curved Sword - prison tower rafters", + "Painting Guardian's Curved Sword", hidden=True), # Invisible walkway + DS3LocationData("AL: Brass Helm - tomb", "Brass Helm", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Armor - tomb", "Brass Armor", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Gauntlets - tomb", "Brass Gauntlets", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Leggings - tomb", "Brass Leggings", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Human Dregs - water reserves", "Human Dregs", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Ember - spiral staircase, bottom", "Ember"), + DS3LocationData("AL: Large Titanite Shard - bottom of the furthest buttress", + "Large Titanite Shard"), + DS3LocationData("AL: Large Titanite Shard - right after light cathedral", + "Large Titanite Shard"), + DS3LocationData("AL: Large Titanite Shard - walkway, side path by cathedral", + "Large Titanite Shard"), + DS3LocationData("AL: Soul of a Weary Warrior - plaza, nearer", "Soul of a Weary Warrior"), + DS3LocationData("AL: Ember - plaza, right side", "Ember"), + DS3LocationData("AL: Ember - plaza, further", "Ember"), + DS3LocationData("AL: Large Titanite Shard - balcony by dead giants", + "Large Titanite Shard"), + DS3LocationData("AL: Dark Stoneplate Ring - by dark stairs up from plaza", + "Dark Stoneplate Ring"), + DS3LocationData("AL: Large Titanite Shard - bottom of the nearest buttress", + "Large Titanite Shard"), + DS3LocationData("AL: Deep Gem - water reserves", "Deep Gem"), + DS3LocationData("AL: Titanite Scale - top of ladder up to buttresses", "Titanite Scale"), + DS3LocationData("AL: Dragonslayer Greatarrow - drop from nearest buttress", + "Dragonslayer Greatarrow x5", static='06,0:53700620::', + hidden=True), # Hidden fall + DS3LocationData("AL: Dragonslayer Greatbow - drop from nearest buttress", + "Dragonslayer Greatbow", static='06,0:53700620::', + hidden=True), # Hidden fall + DS3LocationData("AL: Easterner's Ashes - below top of furthest buttress", + "Easterner's Ashes", progression=True), + DS3LocationData("AL: Painting Guardian Hood - prison tower, rafters", + "Painting Guardian Hood", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Gown - prison tower, rafters", + "Painting Guardian Gown", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Gloves - prison tower, rafters", + "Painting Guardian Gloves", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Waistcloth - prison tower, rafters", + "Painting Guardian Waistcloth", hidden=True), # Invisible walkway + DS3LocationData("AL: Soul of a Crestfallen Knight - right of dark cathedral entrance", + "Soul of a Crestfallen Knight"), + DS3LocationData("AL: Moonlight Arrow - dark cathedral, up right stairs", + "Moonlight Arrow x6"), + DS3LocationData("AL: Proof of a Concord Kept - dark cathedral, up left stairs", + "Proof of a Concord Kept"), + DS3LocationData("AL: Large Soul of a Weary Warrior - left of dark cathedral entrance", + "Large Soul of a Weary Warrior"), + DS3LocationData("AL: Giant's Coal - by giant near dark cathedral", "Giant's Coal"), + DS3LocationData("AL: Havel's Ring+2 - prison tower, rafters", "Havel's Ring+2", ngp=True, + hidden=True), # Invisible walkway + DS3LocationData("AL: Ring of Favor+1 - light cathedral, upstairs", "Ring of Favor+1", + ngp=True), + DS3LocationData("AL: Sun Princess Ring - dark cathedral, after boss", "Sun Princess Ring"), + DS3LocationData("AL: Reversal Ring - tomb, chest in corner", "Reversal Ring", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Golden Ritual Spear - light cathedral, mimic upstairs", + "Golden Ritual Spear", mimic=True), + DS3LocationData("AL: Ring of Favor - water reserves, both minibosses", "Ring of Favor", + miniboss=True, + hidden=True), # Sulyvahn's Beast Duo drop, behind illusory wall + DS3LocationData("AL: Blade of the Darkmoon - Yorshka with Darkmoon Loyalty", + "Blade of the Darkmoon", missable=True, drop=True, + npc=True), # Hidden walkway, missable because it will break Sirris's quest + DS3LocationData("AL: Simple Gem - light cathedral, lizard upstairs", "Simple Gem", + lizard=True), + DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #1", + "Twinkling Titanite", lizard=True), + DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2", + "Twinkling Titanite", lizard=True), + DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby", + miniboss=True), # Deep Accursed drop + DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful", + hidden=True), # Behind illusory wall + + DS3LocationData("FS: Budding Green Blossom - shop after killing Creighton and AL boss", + "Budding Green Blossom", static='99,0:-1:110000,70000118:', + missable=True, npc=True, + shop=True), # sold by Shrine Maiden after killing Aldrich and helping + # Sirris defeat Creighton + + # Sirris (quest completion) + DS3LocationData("FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "Sunset Shield", missable=True, hostile_npc=True, npc=True), + # In Pit of Hollows after killing Hodrick + DS3LocationData("US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "Sunset Helm", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "Sunset Armor", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "Sunset Gauntlets", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + "Sunset Leggings", missable=True, hostile_npc=True, npc=True), + + # Shrine Handmaid after killing Sulyvahn's Beast Duo + DS3LocationData("FS: Helm of Favor - shop after killing water reserve minibosses", + "Helm of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Embraced Armor of Favor - shop after killing water reserve minibosses", + "Embraced Armor of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Gauntlets of Favor - shop after killing water reserve minibosses", + "Gauntlets of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Leggings of Favor - shop after killing water reserve minibosses", + "Leggings of Favor", hidden=True, miniboss=True, shop=True), + + # Anri of Astora + DS3LocationData("AL: Chameleon - tomb after marrying Anri", "Chameleon", missable=True, + npc=True), + DS3LocationData("AL: Anri's Straight Sword - Anri quest", "Anri's Straight Sword", + missable=True, npc=True), + + # Shrine Handmaid after killing Ringfinger Leonhard + # This is listed here even though you can kill Leonhard immediately because we want the + # logic to assume people will do his full quest. Missable because he can disappear forever + # if you use up all your Pale Tongues. + DS3LocationData("FS: Leonhard's Garb - shop after killing Leonhard", + "Leonhard's Garb", hidden=True, npc=True, shop=True, missable=True), + DS3LocationData("FS: Leonhard's Gauntlets - shop after killing Leonhard", + "Leonhard's Gauntlets", hidden=True, npc=True, shop=True, + missable=True), + DS3LocationData("FS: Leonhard's Trousers - shop after killing Leonhard", + "Leonhard's Trousers", hidden=True, npc=True, shop=True, + missable=True), + + # Shrine Handmaid after killing Alrich, Devourer of Gods + DS3LocationData("FS: Smough's Helm - shop after killing AL boss", "Smough's Helm", + boss=True, shop=True), + DS3LocationData("FS: Smough's Armor - shop after killing AL boss", "Smough's Armor", + boss=True, shop=True), + DS3LocationData("FS: Smough's Gauntlets - shop after killing AL boss", "Smough's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Smough's Leggings - shop after killing AL boss", "Smough's Leggings", + boss=True, shop=True), + + # Ringfinger Leonhard (quest or kill) + DS3LocationData("AL: Crescent Moon Sword - Leonhard drop", "Crescent Moon Sword", + missable=True, npc=True), + DS3LocationData("AL: Silver Mask - Leonhard drop", "Silver Mask", missable=True, + npc=True), + DS3LocationData("AL: Soul of Rosaria - Leonhard drop", "Soul of Rosaria", missable=True, + npc=True), + + # Shrine Handmaid after killing Anri or completing their quest + DS3LocationData("FS: Elite Knight Helm - shop after Anri quest", "Elite Knight Helm", + npc=True, shop=True), + DS3LocationData("FS: Elite Knight Armor - shop after Anri quest", "Elite Knight Armor", + npc=True, shop=True), + DS3LocationData("FS: Elite Knight Gauntlets - shop after Anri quest", + "Elite Knight Gauntlets", npc=True, shop=True), + DS3LocationData("FS: Elite Knight Leggings - shop after Anri quest", + "Elite Knight Leggings", npc=True, shop=True), ], "Lothric Castle": [ - DS3LocationData("LC: Hood of Prayer", "Hood of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Robe of Prayer", "Robe of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Skirt of Prayer", "Skirt of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Sacred Bloom Shield", "Sacred Bloom Shield", DS3LocationCategory.SHIELD), - DS3LocationData("LC: Winged Knight Helm", "Winged Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Armor", "Winged Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Gauntlets", "Winged Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Leggings", "Winged Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Greatlance", "Greatlance", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Sniper Crossbow", "Sniper Crossbow", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Spirit Tree Crest Shield", "Spirit Tree Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("LC: Red Tearstone Ring", "Red Tearstone Ring", DS3LocationCategory.RING), - DS3LocationData("LC: Caitha's Chime", "Caitha's Chime", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Braille Divine Tome of Lothric", "Braille Divine Tome of Lothric", DS3LocationCategory.MISC), - DS3LocationData("LC: Knight's Ring", "Knight's Ring", DS3LocationCategory.RING), - DS3LocationData("LC: Irithyll Rapier", "Irithyll Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Sunlight Straight Sword", "Sunlight Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", DS3LocationCategory.BOSS), - DS3LocationData("LC: Grand Archives Key", "Grand Archives Key", DS3LocationCategory.KEY), - DS3LocationData("LC: Gotthard Twinswords", "Gotthard Twinswords", DS3LocationCategory.WEAPON), + DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", + prominent=True, boss=True), + DS3LocationData("LC: Sniper Bolt - moat, right path end", "Sniper Bolt x11"), + DS3LocationData("LC: Sniper Crossbow - moat, right path end", "Sniper Crossbow"), + DS3LocationData("LC: Titanite Scale - dark room, upper balcony", "Titanite Scale"), + DS3LocationData("LC: Titanite Chunk - dark room mid, out door opposite wyvern", + "Titanite Chunk"), + DS3LocationData("LC: Greatlance - overlooking Dragon Barracks bonfire", "Greatlance"), + DS3LocationData("LC: Titanite Chunk - ascent, first balcony", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - ascent, turret before barricades", "Titanite Chunk"), + DS3LocationData("LC: Sacred Bloom Shield - ascent, behind illusory wall", + "Sacred Bloom Shield", hidden=True), # Behind illusory wall + DS3LocationData("LC: Titanite Chunk - ascent, final turret", "Titanite Chunk x2"), + DS3LocationData("LC: Refined Gem - plaza", "Refined Gem"), + DS3LocationData("LC: Soul of a Crestfallen Knight - by lift bottom", + "Soul of a Crestfallen Knight"), + DS3LocationData("LC: Undead Bone Shard - moat, far ledge", "Undead Bone Shard"), + DS3LocationData("LC: Lightning Urn - moat, right path, first room", "Lightning Urn x3"), + DS3LocationData("LC: Titanite Chunk - moat #1", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - moat #2", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - moat, near ledge", "Titanite Chunk"), + DS3LocationData("LC: Caitha's Chime - chapel, drop onto roof", "Caitha's Chime"), + DS3LocationData("LC: Lightning Urn - plaza", "Lightning Urn x6"), + DS3LocationData("LC: Ember - plaza, by gate", "Ember"), + DS3LocationData("LC: Raw Gem - plaza left", "Raw Gem"), + DS3LocationData("LC: Black Firebomb - dark room lower", "Black Firebomb x3"), + DS3LocationData("LC: Pale Pine Resin - dark room upper, by mimic", "Pale Pine Resin"), + DS3LocationData("LC: Large Soul of a Weary Warrior - main hall, by lever", + "Large Soul of a Weary Warrior"), + DS3LocationData("LC: Sunlight Medal - by lift top", "Sunlight Medal"), + DS3LocationData("LC: Soul of a Crestfallen Knight - wyvern room, balcony", + "Soul of a Crestfallen Knight", hidden=True), # Hidden fall + DS3LocationData("LC: Titanite Chunk - altar roof", "Titanite Chunk"), + DS3LocationData("LC: Titanite Scale - dark room mid, out door opposite wyvern", + "Titanite Scale"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - moat, right path", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Knight's Ring - altar", "Knight's Ring"), + DS3LocationData("LC: Ember - main hall, left of stairs", "Ember"), + DS3LocationData("LC: Large Soul of a Weary Warrior - ascent, last turret", + "Large Soul of a Weary Warrior"), + DS3LocationData("LC: Ember - by Dragon Barracks bonfire", "Ember"), + DS3LocationData("LC: Twinkling Titanite - ascent, side room", "Twinkling Titanite"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - dark room mid", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Ember - plaza center", "Ember"), + DS3LocationData("LC: Winged Knight Helm - ascent, behind illusory wall", + "Winged Knight Helm", hidden=True), + DS3LocationData("LC: Winged Knight Armor - ascent, behind illusory wall", + "Winged Knight Armor", hidden=True), + DS3LocationData("LC: Winged Knight Gauntlets - ascent, behind illusory wall", + "Winged Knight Gauntlets", hidden=True), + DS3LocationData("LC: Winged Knight Leggings - ascent, behind illusory wall", + "Winged Knight Leggings", hidden=True), + DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"), + DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room", + "Braille Divine Tome of Lothric", hidden=True), # Hidden fall + DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"), + DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"), + DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"), + DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"), + DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"), + DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"), + DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest", + "Spirit Tree Crest Shield"), + DS3LocationData("LC: Titanite Scale - basement, chest", "Titanite Scale"), + DS3LocationData("LC: Twinkling Titanite - basement, chest #1", "Twinkling Titanite"), + DS3LocationData("LC: Twinkling Titanite - basement, chest #2", "Twinkling Titanite x2"), + DS3LocationData("LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop down", + "Life Ring+2", ngp=True, hidden=True), # Hidden fall + DS3LocationData("LC: Dark Stoneplate Ring+1 - wyvern room, balcony", + "Dark Stoneplate Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("LC: Thunder Stoneplate Ring+2 - chapel, drop onto roof", + "Thunder Stoneplate Ring+2", ngp=True), + DS3LocationData("LC: Sunlight Straight Sword - wyvern room, mimic", + "Sunlight Straight Sword", mimic=True, hidden=True), # Hidden fall + DS3LocationData("LC: Titanite Scale - dark room, upper, mimic", "Titanite Scale x3", + mimic=True), + DS3LocationData("LC: Ember - wyvern room, wyvern foot mob drop", "Ember x2", + drop=True, hidden=True), # Hidden fall, Pus of Man Wyvern drop + DS3LocationData("LC: Titanite Chunk - wyvern room, wyvern foot mob drop", "Titanite Chunk x2", + drop=True, hidden=True), # Hidden fall, Pus of Man Wyvern drop + DS3LocationData("LC: Ember - dark room mid, pus of man mob drop", "Ember x2", + drop=True), # Pus of Man Wyvern drop + DS3LocationData("LC: Titanite Chunk - dark room mid, pus of man mob drop", + "Titanite Chunk x2"), + DS3LocationData("LC: Irithyll Rapier - basement, miniboss drop", "Irithyll Rapier", + miniboss=True), # Boreal Outrider drop + DS3LocationData("LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizard", + "Twinkling Titanite x2", lizard=True, missable=True), + DS3LocationData("LC: Twinkling Titanite - moat, right path, lizard", + "Twinkling Titanite x2", lizard=True, missable=True), + DS3LocationData("LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bosses", + "Gotthard Twinswords", conditional=True), + DS3LocationData("LC: Grand Archives Key - by Grand Archives door, after PC and AL bosses", + "Grand Archives Key", prominent=True, progression=True, + conditional=True), + DS3LocationData("LC: Titanite Chunk - down stairs after boss", "Titanite Chunk"), + + # Eygon of Carim (kill or quest) + DS3LocationData("FS: Morne's Great Hammer - Eygon", "Morne's Great Hammer", npc=True), + DS3LocationData("FS: Moaning Shield - Eygon", "Moaning Shield", npc=True), + + # Shrine Handmaid after killing Dragonslayer Armour (or Eygon of Carim) + DS3LocationData("FS: Dancer's Crown - shop after killing LC entry boss", "Dancer's Crown", + boss=True, shop=True), + DS3LocationData("FS: Dancer's Armor - shop after killing LC entry boss", "Dancer's Armor", + boss=True, shop=True), + DS3LocationData("FS: Dancer's Gauntlets - shop after killing LC entry boss", + "Dancer's Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Dancer's Leggings - shop after killing LC entry boss", + "Dancer's Leggings", boss=True, shop=True), + + # Shrine Handmaid after killing Dragonslayer Armour (or Eygon of Carim) + DS3LocationData("FS: Morne's Helm - shop after killing Eygon or LC boss", "Morne's Helm", + boss=True, shop=True), + DS3LocationData("FS: Morne's Armor - shop after killing Eygon or LC boss", "Morne's Armor", + boss=True, shop=True), + DS3LocationData("FS: Morne's Gauntlets - shop after killing Eygon or LC boss", + "Morne's Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Morne's Leggings - shop after killing Eygon or LC boss", + "Morne's Leggings", boss=True, shop=True), ], "Consumed King's Garden": [ - DS3LocationData("CKG: Dragonscale Ring", "Dragonscale Ring", DS3LocationCategory.RING), - DS3LocationData("CKG: Shadow Mask", "Shadow Mask", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Garb", "Shadow Garb", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Gauntlets", "Shadow Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Leggings", "Shadow Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Claw", "Claw", DS3LocationCategory.WEAPON), - DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", DS3LocationCategory.BOSS), - DS3LocationData("CKG: Magic Stoneplate Ring", "Magic Stoneplate Ring", DS3LocationCategory.RING), + DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", + prominent=True, boss=True), + # Could classify this as "hidden" because it's midway down an elevator, but the elevator is + # so slow and the midway point is so obvious that it's not actually hard to find. + DS3LocationData("CKG: Estus Shard - balcony", "Estus Shard"), + DS3LocationData("CKG: Shadow Mask - under center platform", "Shadow Mask"), + DS3LocationData("CKG: Shadow Garb - under rotunda", "Shadow Garb"), + DS3LocationData("CKG: Shadow Gauntlets - under rotunda", "Shadow Gauntlets"), + DS3LocationData("CKG: Shadow Leggings - under rotunda", "Shadow Leggings"), + DS3LocationData("CKG: Black Firebomb - under rotunda", "Black Firebomb x2"), + DS3LocationData("CKG: Claw - under rotunda", "Claw"), + DS3LocationData("CKG: Titanite Chunk - up lone stairway", "Titanite Chunk"), + DS3LocationData("CKG: Dragonscale Ring - shortcut, leave halfway down lift", + "Dragonscale Ring"), + DS3LocationData("CKG: Human Pine Resin - toxic pool, past rotunda", "Human Pine Resin"), + DS3LocationData("CKG: Titanite Chunk - shortcut", "Titanite Chunk"), + DS3LocationData("CKG: Titanite Chunk - balcony, drop onto rubble", "Titanite Chunk"), + DS3LocationData("CKG: Soul of a Weary Warrior - before first lift", + "Soul of a Weary Warrior"), + DS3LocationData("CKG: Dark Gem - under lone stairway", "Dark Gem"), + DS3LocationData("CKG: Titanite Scale - shortcut", "Titanite Scale"), + DS3LocationData("CKG: Human Pine Resin - pool by lift", "Human Pine Resin x2"), + DS3LocationData("CKG: Titanite Chunk - right of shortcut lift bottom", "Titanite Chunk"), + DS3LocationData("CKG: Ring of Sacrifice - under balcony", "Ring of Sacrifice"), + DS3LocationData("CKG: Wood Grain Ring+1 - by first elevator bottom", "Wood Grain Ring+1", + ngp=True), + DS3LocationData("CKG: Sage Ring+2 - balcony, drop onto rubble, jump back", "Sage Ring+2", + ngp=True, hidden=True), + DS3LocationData("CKG: Titanite Scale - tomb, chest #1", "Titanite Scale"), + DS3LocationData("CKG: Titanite Scale - tomb, chest #2", "Titanite Scale"), + DS3LocationData("CKG: Magic Stoneplate Ring - mob drop before boss", + "Magic Stoneplate Ring", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Cathedral Knight + + # After Oceiros's boss room, only once the Drakeblood summon in AP has been killed + DS3LocationData("CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "Drakeblood Helm", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "Drakeblood Armor", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "Drakeblood Gauntlets", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + "Drakeblood Leggings", hostile_npc=True, hidden=True), ], "Grand Archives": [ - DS3LocationData("GA: Avelyn", "Avelyn", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Witch's Locks", "Witch's Locks", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Power Within", "Power Within", DS3LocationCategory.SPELL), - DS3LocationData("GA: Scholar Ring", "Scholar Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Soul Stream", "Soul Stream", DS3LocationCategory.SPELL), - DS3LocationData("GA: Fleshbite Ring", "Fleshbite Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Crystal Chime", "Crystal Chime", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Golden Wing Crest Shield", "Golden Wing Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("GA: Onikiri and Ubadachi", "Onikiri and Ubadachi", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Hunter's Ring", "Hunter's Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Divine Pillars of Light", "Divine Pillars of Light", DS3LocationCategory.SPELL), - DS3LocationData("GA: Cinders of a Lord - Lothric Prince", "Cinders of a Lord - Lothric Prince", DS3LocationCategory.KEY), - DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", DS3LocationCategory.BOSS), - DS3LocationData("GA: Sage's Crystal Staff", "Sage's Crystal Staff", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Outrider Knight Helm", "Outrider Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Armor", "Outrider Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Gauntlets", "Outrider Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Leggings", "Outrider Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Crystal Scroll", "Crystal Scroll", DS3LocationCategory.MISC), + DS3LocationData("GA: Titanite Slab - final elevator secret", "Titanite Slab", + hidden=True), + DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", boss=True), + DS3LocationData("GA: Cinders of a Lord - Lothric Prince", + "Cinders of a Lord - Lothric Prince", + static="09,0:50002040::", prominent=True, progression=True, + boss=True), + DS3LocationData("GA: Onikiri and Ubadachi - outside 5F, NPC drop", "Onikiri and Ubadachi", + hostile_npc=True, # Black Hand Kamui drop + missable=True), # This is placed at the location the NPC gets randomized + # to, which makes it hard to include in logic. + DS3LocationData("GA: Golden Wing Crest Shield - outside 5F, NPC drop", + "Golden Wing Crest Shield", + hostile_npc=True), # Lion Knight Albert drop + DS3LocationData("GA: Sage's Crystal Staff - outside 5F, NPC drop", + "Sage's Crystal Staff", + hostile_npc=True), # Daughter of Crystal Kriemhild drop + DS3LocationData("GA: Titanite Chunk - 1F, up right stairs", "Titanite Chunk"), + DS3LocationData("GA: Titanite Chunk - 1F, path from wax pool", "Titanite Chunk"), + DS3LocationData("GA: Soul of a Crestfallen Knight - 1F, loop left after drop", + "Soul of a Crestfallen Knight"), + DS3LocationData("GA: Titanite Chunk - 1F, balcony", "Titanite Chunk"), + DS3LocationData("GA: Fleshbite Ring - up stairs from 4F", "Fleshbite Ring"), + DS3LocationData("GA: Soul of a Crestfallen Knight - path to dome", + "Soul of a Crestfallen Knight"), + DS3LocationData("GA: Soul of a Nameless Soldier - dark room", "Soul of a Nameless Soldier"), + DS3LocationData("GA: Crystal Chime - 1F, path from wax pool", "Crystal Chime"), + DS3LocationData("GA: Titanite Scale - dark room, upstairs", "Titanite Scale"), + DS3LocationData("GA: Estus Shard - dome, far balcony", "Estus Shard"), + DS3LocationData("GA: Homeward Bone - 2F early balcony", "Homeward Bone x3"), + DS3LocationData("GA: Titanite Scale - 2F, titanite scale atop bookshelf", "Titanite Scale", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - 2F, by wax pool", "Titanite Chunk"), + DS3LocationData("GA: Hollow Gem - rooftops lower, in hall", "Hollow Gem", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Scale - 3F, corner up stairs", "Titanite Scale"), + DS3LocationData("GA: Titanite Scale - 1F, up stairs on bookshelf", "Titanite Scale"), + DS3LocationData("GA: Titanite Scale - 3F, by ladder to 2F late", "Titanite Scale", + hidden=True), # Hidden by a table + DS3LocationData("GA: Shriving Stone - 2F late, by ladder from 3F", "Shriving Stone"), + DS3LocationData("GA: Large Soul of a Crestfallen Knight - 4F, back", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("GA: Titanite Chunk - rooftops, balcony", "Titanite Chunk"), + DS3LocationData("GA: Titanite Scale - rooftops lower, path to 2F", "Titanite Scale x3", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - rooftops lower, ledge by buttress", "Titanite Chunk", + hidden=True), # Hidden fall + DS3LocationData("GA: Soul of a Weary Warrior - rooftops, by lizards", + "Soul of a Weary Warrior"), + DS3LocationData("GA: Titanite Chunk - rooftops, just before 5F", "Titanite Chunk"), + DS3LocationData("GA: Ember - 5F, by entrance", "Ember"), + DS3LocationData("GA: Blessed Gem - rafters", "Blessed Gem"), + DS3LocationData("GA: Titanite Chunk - 5F, far balcony", "Titanite Chunk x2"), + DS3LocationData("GA: Large Soul of a Crestfallen Knight - outside 5F", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("GA: Avelyn - 1F, drop from 3F onto bookshelves", "Avelyn", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - 2F, right after dark room", "Titanite Chunk"), + DS3LocationData("GA: Hunter's Ring - dome, very top", "Hunter's Ring"), + DS3LocationData("GA: Divine Pillars of Light - cage above rafters", + "Divine Pillars of Light"), + DS3LocationData("GA: Power Within - dark room, behind retractable bookshelf", + "Power Within", hidden=True), # Switch in darkened room + DS3LocationData("GA: Sage Ring+1 - rafters, second level down", "Sage Ring+1", ngp=True), + DS3LocationData("GA: Lingering Dragoncrest Ring+2 - dome, room behind spire", + "Lingering Dragoncrest Ring+2", ngp=True), + DS3LocationData("GA: Divine Blessing - rafters, down lower level ladder", + "Divine Blessing"), + DS3LocationData("GA: Twinkling Titanite - rafters, down lower level ladder", + "Twinkling Titanite x3"), + DS3LocationData("GA: Witch's Locks - dark room, behind retractable bookshelf", + "Witch's Locks", hidden=True), # Switch in darkened room + DS3LocationData("GA: Titanite Slab - 1F, after pulling 2F switch", "Titanite Slab", + hidden=True), + DS3LocationData("GA: Titanite Scale - 4F, chest by exit", "Titanite Scale x3"), + DS3LocationData("GA: Soul Stream - 3F, behind illusory wall", "Soul Stream", + hidden=True), # Behind illusory wall + DS3LocationData("GA: Scholar Ring - 2F, between late and early", "Scholar Ring"), + DS3LocationData("GA: Undead Bone Shard - 5F, by entrance", "Undead Bone Shard"), + DS3LocationData("GA: Titanite Slab - dome, kill all mobs", "Titanite Slab", + drop=True, + hidden=True), # Guaranteed drop from killing all Winged Knights + DS3LocationData("GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Helm", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Armor", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Gauntlets", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Leggings", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Crystal Scroll - 2F late, miniboss drop", "Crystal Scroll", + miniboss=True), # Crystal Sage drop + DS3LocationData("GA: Twinkling Titanite - dark room, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Chaos Gem - dark room, lizard", "Chaos Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - 1F, lizard by drop", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Crystal Gem - 1F, lizard by drop", "Crystal Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - 2F, lizard by entrance", "Twinkling Titanite x2", + lizard=True), + DS3LocationData("GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizard", + "Titanite Scale x2", lizard=True, hidden=True), # Hidden fall + DS3LocationData("GA: Twinkling Titanite - rooftops, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Heavy Gem - rooftops, lizard", "Heavy Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - rooftops, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Sharp Gem - rooftops, lizard", "Sharp Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - up stairs from 4F, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Refined Gem - up stairs from 4F, lizard", "Refined Gem", + lizard=True), + DS3LocationData("GA: Twinkling Titanite - dark room, lizard #2", "Twinkling Titanite x2", + lizard=True), + + # Shrine Handmaid after killing NPCs + DS3LocationData("FS: Faraam Helm - shop after killing GA NPC", "Faraam Helm", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Armor - shop after killing GA NPC", "Faraam Armor", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Gauntlets - shop after killing GA NPC", "Faraam Gauntlets", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Boots - shop after killing GA NPC", "Faraam Boots", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Black Hand Hat - shop after killing GA NPC", "Black Hand Hat", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Black Hand Armor - shop after killing GA NPC", "Black Hand Armor", + hidden=True, hostile_npc=True, shop=True), + + # Shrine Handmaid after killing Lothric, Younger Prince + DS3LocationData("FS: Lorian's Helm - shop after killing GA boss", "Lorian's Helm", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Armor - shop after killing GA boss", "Lorian's Armor", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Gauntlets - shop after killing GA boss", "Lorian's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Leggings - shop after killing GA boss", "Lorian's Leggings", + boss=True, shop=True), + + # Sirris quest completion + beat Twin Princes + DS3LocationData("FS: Sunless Talisman - Sirris, kill GA boss", "Sunless Talisman", + missable=True, npc=True), + DS3LocationData("FS: Sunless Veil - shop, Sirris quest, kill GA boss", "Sunless Veil", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Armor - shop, Sirris quest, kill GA boss", "Sunless Armor", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "Sunless Gauntlets", missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + "Sunless Leggings", missable=True, npc=True, shop=True), + + # Unbreakable Patches + DS3LocationData("FS: Hidden Blessing - Patches after searching GA", "Hidden Blessing", + missable=True, npc=True, shop=True), ], "Untended Graves": [ - DS3LocationData("UG: Ashen Estus Ring", "Ashen Estus Ring", DS3LocationCategory.RING), - DS3LocationData("UG: Black Knight Glaive", "Black Knight Glaive", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Hornet Ring", "Hornet Ring", DS3LocationCategory.RING), - DS3LocationData("UG: Chaos Blade", "Chaos Blade", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Blacksmith Hammer", "Blacksmith Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Eyes of a Fire Keeper", "Eyes of a Fire Keeper", DS3LocationCategory.KEY), - DS3LocationData("UG: Coiled Sword Fragment", "Coiled Sword Fragment", DS3LocationCategory.MISC), - DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", DS3LocationCategory.BOSS), + DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", prominent=True, + boss=True), + DS3LocationData("UG: Priestess Ring - shop", "Priestess Ring", shop=True), + DS3LocationData("UG: Shriving Stone - swamp, by bonfire", "Shriving Stone"), + DS3LocationData("UG: Titanite Chunk - swamp, left path by fountain", "Titanite Chunk"), + DS3LocationData("UG: Soul of a Crestfallen Knight - swamp, center", + "Soul of a Crestfallen Knight"), + DS3LocationData("UG: Titanite Chunk - swamp, right path by fountain", "Titanite Chunk"), + DS3LocationData("UG: Ashen Estus Ring - swamp, path opposite bonfire", "Ashen Estus Ring"), + DS3LocationData("UG: Black Knight Glaive - boss arena", "Black Knight Glaive"), + DS3LocationData("UG: Hidden Blessing - cemetery, behind coffin", "Hidden Blessing"), + DS3LocationData("UG: Eyes of a Fire Keeper - shrine, Irina's room", "Eyes of a Fire Keeper", + hidden=True), # Illusory wall + DS3LocationData("UG: Soul of a Crestfallen Knight - environs, above shrine entrance", + "Soul of a Crestfallen Knight"), + DS3LocationData("UG: Blacksmith Hammer - shrine, Andre's room", "Blacksmith Hammer"), + DS3LocationData("UG: Chaos Blade - environs, left of shrine", "Chaos Blade"), + DS3LocationData("UG: Hornet Ring - environs, right of main path after killing FK boss", + "Hornet Ring", conditional=True), + DS3LocationData("UG: Coiled Sword Fragment - shrine, dead bonfire", "Coiled Sword Fragment", + boss=True), + DS3LocationData("UG: Life Ring+3 - shrine, behind big throne", "Life Ring+3", ngp=True), + DS3LocationData("UG: Ring of Steel Protection+1 - environs, behind bell tower", + "Ring of Steel Protection+1", ngp=True), + + # Yuria shop, or Shrine Handmaiden with Hollow's Ashes + # This is here because this is where the ashes end up if you kill Yoel or Yuria + DS3LocationData("FS: Ring of Sacrifice - Yuria shop", "Ring of Sacrifice", + static='99,0:-1:40000,110000,70000107,70000116:', npc=True, + shop=True), + + # Untended Graves Handmaid + # All shop items are missable because she can be killed, except Priestess ring because she + # drops it on death anyway. + DS3LocationData("UG: Ember - shop", "Ember", shop=True, missable=True), + # Untended Graves Handmaid after killing Abyss Watchers + DS3LocationData("UG: Wolf Knight Helm - shop after killing FK boss", "Wolf Knight Helm", + boss=True, shop=True, conditional=True, + missable=True), + DS3LocationData("UG: Wolf Knight Armor - shop after killing FK boss", + "Wolf Knight Armor", boss=True, shop=True, missable=True), + DS3LocationData("UG: Wolf Knight Gauntlets - shop after killing FK boss", + "Wolf Knight Gauntlets", boss=True, shop=True, missable=True), + DS3LocationData("UG: Wolf Knight Leggings - shop after killing FK boss", + "Wolf Knight Leggings", boss=True, shop=True, missable=True), + + # Shrine Handmaid after killing Champion Gundyr + DS3LocationData("FS: Gundyr's Helm - shop after killing UG boss", "Gundyr's Helm", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Armor - shop after killing UG boss", "Gundyr's Armor", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Gauntlets - shop after killing UG boss", "Gundyr's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Leggings - shop after killing UG boss", "Gundyr's Leggings", + boss=True, shop=True), ], "Archdragon Peak": [ - DS3LocationData("AP: Lightning Clutch Ring", "Lightning Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Ancient Dragon Greatshield", "Ancient Dragon Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("AP: Ring of Steel Protection", "Ring of Steel Protection", DS3LocationCategory.RING), - DS3LocationData("AP: Calamity Ring", "Calamity Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Drakeblood Greatsword", "Drakeblood Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Dragonslayer Spear", "Dragonslayer Spear", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Thunder Stoneplate Ring", "Thunder Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Great Magic Barrier", "Great Magic Barrier", DS3LocationCategory.SPELL), - DS3LocationData("AP: Dragon Chaser's Ashes", "Dragon Chaser's Ashes", DS3LocationCategory.MISC), - DS3LocationData("AP: Twinkling Dragon Torso Stone", "Twinkling Dragon Torso Stone", DS3LocationCategory.MISC), - DS3LocationData("AP: Dragonslayer Helm", "Dragonslayer Helm", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Armor", "Dragonslayer Armor", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Gauntlets", "Dragonslayer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Leggings", "Dragonslayer Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Ricard's Rapier", "Ricard's Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", DS3LocationCategory.BOSS), - DS3LocationData("AP: Dragon Tooth", "Dragon Tooth", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Havel's Greatshield", "Havel's Greatshield", DS3LocationCategory.SHIELD), + DS3LocationData("AP: Dragon Head Stone - fort, boss drop", "Dragon Head Stone", + prominent=True, boss=True), + DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", + prominent=True, boss=True), + DS3LocationData("AP: Dragon Tooth - belfry roof, NPC drop", "Dragon Tooth", + hostile_npc=True), # Havel Knight drop + DS3LocationData("AP: Havel's Greatshield - belfry roof, NPC drop", "Havel's Greatshield", + hostile_npc=True), # Havel Knight drop + DS3LocationData("AP: Drakeblood Greatsword - mausoleum, NPC drop", "Drakeblood Greatsword", + hostile_npc=True), + DS3LocationData("AP: Ricard's Rapier - belfry, NPC drop", "Ricard's Rapier", + hostile_npc=True), + DS3LocationData("AP: Lightning Clutch Ring - intro, left of boss door", + "Lightning Clutch Ring"), + DS3LocationData("AP: Stalk Dung Pie - fort overlook", "Stalk Dung Pie x6"), + DS3LocationData("AP: Titanite Chunk - fort, second room balcony", "Titanite Chunk"), + DS3LocationData("AP: Titanite Scale - mausoleum, downstairs balcony #1", + "Titanite Scale"), + DS3LocationData("AP: Soul of a Weary Warrior - intro, first cliff edge", + "Soul of a Weary Warrior"), + DS3LocationData("AP: Titanite Chunk - intro, left before archway", "Titanite Chunk"), + DS3LocationData("AP: Lightning Gem - intro, side rise", "Lightning Gem"), + DS3LocationData("AP: Homeward Bone - intro, path to bonfire", "Homeward Bone x2"), + DS3LocationData("AP: Soul of a Nameless Soldier - intro, right before archway", + "Soul of a Nameless Soldier"), + DS3LocationData("AP: Titanite Chunk - intro, archway corner", "Titanite Chunk"), + DS3LocationData("AP: Ember - fort overlook #1", "Ember"), + DS3LocationData("AP: Large Soul of a Weary Warrior - fort, center", + "Large Soul of a Weary Warrior"), + DS3LocationData("AP: Large Soul of a Nameless Soldier - fort, by stairs to first room", + "Large Soul of a Nameless Soldier"), + DS3LocationData("AP: Lightning Urn - fort, left of first room entrance", + "Lightning Urn x4"), + DS3LocationData("AP: Lightning Bolt - rotunda", "Lightning Bolt x12"), + DS3LocationData("AP: Titanite Chunk - rotunda", "Titanite Chunk x2"), + # Not 100% sure about this location name, can't find this on any maps + DS3LocationData("AP: Dung Pie - fort, landing after second room", "Dung Pie x3"), + DS3LocationData("AP: Titanite Scale - mausoleum, downstairs balcony #2", "Titanite Scale"), + DS3LocationData("AP: Soul of a Weary Warrior - walkway, building window", + "Soul of a Weary Warrior"), + DS3LocationData("AP: Soul of a Crestfallen Knight - mausoleum, upstairs", + "Soul of a Crestfallen Knight"), + DS3LocationData("AP: Titanite Chunk - intro, behind rock", "Titanite Chunk"), + DS3LocationData("AP: Ember - fort overlook #2", "Ember"), + DS3LocationData("AP: Thunder Stoneplate Ring - walkway, up ladder", + "Thunder Stoneplate Ring"), + DS3LocationData("AP: Titanite Scale - mausoleum, upstairs balcony", "Titanite Scale"), + DS3LocationData("AP: Ember - belfry, below bell", "Ember"), + DS3LocationData("AP: Ancient Dragon Greatshield - intro, on archway", + "Ancient Dragon Greatshield"), + DS3LocationData("AP: Large Soul of a Crestfallen Knight - summit, by fountain", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("AP: Dragon Chaser's Ashes - summit, side path", "Dragon Chaser's Ashes", + progression=True), + DS3LocationData("AP: Ember - intro, by bonfire", "Ember"), + DS3LocationData("AP: Dragonslayer Spear - gate after mausoleum", "Dragonslayer Spear"), + DS3LocationData("AP: Dragonslayer Helm - plaza", "Dragonslayer Helm"), + DS3LocationData("AP: Dragonslayer Armor - plaza", "Dragonslayer Armor"), + DS3LocationData("AP: Dragonslayer Gauntlets - plaza", "Dragonslayer Gauntlets"), + DS3LocationData("AP: Dragonslayer Leggings - plaza", "Dragonslayer Leggings"), + DS3LocationData("AP: Twinkling Titanite - fort, end of rafters", "Twinkling Titanite x2"), + DS3LocationData("AP: Twinkling Titanite - fort, down second room balcony ladder", + "Twinkling Titanite x2"), + DS3LocationData("AP: Titanite Slab - belfry roof", "Titanite Slab"), + DS3LocationData("AP: Great Magic Barrier - drop off belfry roof", "Great Magic Barrier", + hidden=True), # Hidden fall + DS3LocationData("AP: Titanite Slab - plaza", "Titanite Slab"), + DS3LocationData("AP: Ring of Steel Protection - fort overlook, beside stairs", + "Ring of Steel Protection"), + DS3LocationData("AP: Havel's Ring+1 - summit, after building", "Havel's Ring+1", + ngp=True), + DS3LocationData("AP: Covetous Gold Serpent Ring+2 - plaza", "Covetous Gold Serpent Ring+2", + ngp=True), + DS3LocationData("AP: Titanite Scale - walkway building", "Titanite Scale x3"), + DS3LocationData("AP: Twinkling Titanite - belfry, by ladder to roof", + "Twinkling Titanite x3"), + DS3LocationData("AP: Twinkling Dragon Torso Stone - summit, gesture at altar", + "Twinkling Dragon Torso Stone", hidden=True), # Requires gesture + DS3LocationData("AP: Calamity Ring - mausoleum, gesture at altar", "Calamity Ring", + hidden=True), # Requires gesture + DS3LocationData("AP: Twinkling Titanite - walkway building, lizard", + "Twinkling Titanite x3", lizard=True), + DS3LocationData("AP: Titanite Chunk - walkway, miniboss drop", "Titanite Chunk x6", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("AP: Titanite Scale - walkway, miniboss drop", "Titanite Scale x3", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("AP: Twinkling Titanite - walkway, miniboss drop", "Twinkling Titanite x3", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("FS: Hawkwood's Swordgrass - Andre after gesture in AP summit", + "Hawkwood's Swordgrass", conditional=True, hidden=True), + + # Shrine Handmaid after killing Nameless King + DS3LocationData("FS: Golden Crown - shop after killing AP boss", "Golden Crown", + boss=True, shop=True), + DS3LocationData("FS: Dragonscale Armor - shop after killing AP boss", "Dragonscale Armor", + boss=True, shop=True), + DS3LocationData("FS: Golden Bracelets - shop after killing AP boss", "Golden Bracelets", + boss=True, shop=True), + DS3LocationData("FS: Dragonscale Waistcloth - shop after killing AP boss", + "Dragonscale Waistcloth", boss=True, shop=True), + DS3LocationData("FK: Twinkling Dragon Head Stone - Hawkwood drop", + "Twinkling Dragon Head Stone", missable=True, + npc=True), # Hawkwood (quest) + ], + "Kiln of the First Flame": [ + DS3LocationData("KFF: Soul of the Lords", "Soul of the Lords", boss=True), + + # Shrine Handmaid after placing all Cinders of a Lord + DS3LocationData("FS: Titanite Slab - shop after placing all Cinders", "Titanite Slab", + static='99,0:-1:9210,110000:', hidden=True), + DS3LocationData("FS: Firelink Helm - shop after placing all Cinders", "Firelink Helm", + boss=True, shop=True), + DS3LocationData("FS: Firelink Armor - shop after placing all Cinders", "Firelink Armor", + boss=True, shop=True), + DS3LocationData("FS: Firelink Gauntlets - shop after placing all Cinders", + "Firelink Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Firelink Leggings - shop after placing all Cinders", + "Firelink Leggings", boss=True, shop=True), + + # Yuria (quest, after Soul of Cinder) + DS3LocationData("FS: Billed Mask - Yuria after killing KFF boss", "Billed Mask", + missable=True, npc=True), + DS3LocationData("FS: Black Dress - Yuria after killing KFF boss", "Black Dress", + missable=True, npc=True), + DS3LocationData("FS: Black Gauntlets - Yuria after killing KFF boss", "Black Gauntlets", + missable=True, npc=True), + DS3LocationData("FS: Black Leggings - Yuria after killing KFF boss", "Black Leggings", + missable=True, npc=True), ], - "Kiln of the First Flame": [], # DLC - "Painted World of Ariandel 1": [ - DS3LocationData("PW: Follower Javelin", "Follower Javelin", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Frozen Weapon", "Frozen Weapon", DS3LocationCategory.SPELL), - DS3LocationData("PW: Millwood Greatbow", "Millwood Greatbow", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Captain's Ashes", "Captain's Ashes", DS3LocationCategory.MISC), - DS3LocationData("PW: Millwood Battle Axe", "Millwood Battle Axe", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Ethereal Oak Shield", "Ethereal Oak Shield", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Crow Quills", "Crow Quills", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Slave Knight Hood", "Slave Knight Hood", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Armor", "Slave Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Gauntlets", "Slave Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Leggings", "Slave Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Way of White Corona", "Way of White Corona", DS3LocationCategory.SPELL), - DS3LocationData("PW: Crow Talons", "Crow Talons", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Onyx Blade", "Onyx Blade", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Contraption Key", "Contraption Key", DS3LocationCategory.KEY), + "Painted World of Ariandel (Before Contraption)": [ + DS3LocationData("PW1: Valorheart - boss drop", "Valorheart", prominent=True, boss=True), + DS3LocationData("PW1: Contraption Key - library, NPC drop", "Contraption Key", + prominent=True, progression=True, + hostile_npc=True), # Sir Vilhelm drop + DS3LocationData("PW1: Onyx Blade - library, NPC drop", "Onyx Blade", + hostile_npc=True), # Sir Vilhelm drop + DS3LocationData("PW1: Chillbite Ring - Friede", "Chillbite Ring", + npc=True), # Friede conversation + DS3LocationData("PW1: Rime-blue Moss Clump - snowfield upper, starting cave", + "Rime-blue Moss Clump x2"), + DS3LocationData("PW1: Poison Gem - snowfield upper, forward from bonfire", "Poison Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, path back up", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Follower Javelin - snowfield lower, path back up", "Follower Javelin"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, path to village", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Homeward Bone - snowfield village, outcropping", "Homeward Bone x6"), + DS3LocationData("PW1: Blessed Gem - snowfield, behind tower", "Blessed Gem", + hidden=True), # Hidden behind a tower + DS3LocationData("PW1: Captain's Ashes - snowfield tower, 6F", "Captain's Ashes", + progression=True), + DS3LocationData("PW1: Black Firebomb - snowfield lower, path to bonfire", + "Black Firebomb x2"), + DS3LocationData("PW1: Shriving Stone - below bridge near", "Shriving Stone"), + DS3LocationData("PW1: Millwood Greatarrow - snowfield village, loop back to lower", + "Millwood Greatarrow x5"), + DS3LocationData("PW1: Millwood Greatbow - snowfield village, loop back to lower", + "Millwood Greatbow"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield upper", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Rusted Coin - snowfield lower, straight from fall", "Rusted Coin"), + DS3LocationData("PW1: Large Titanite Shard - snowfield lower, left from fall", + "Large Titanite Shard"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliff", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Crow Quills - settlement loop, jump into courtyard", "Crow Quills", + hidden=True), # Hidden fall + DS3LocationData("PW1: Simple Gem - settlement, lowest level, behind gate", "Simple Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfire", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Slave Knight Hood - settlement roofs, drop by ladder", + "Slave Knight Hood"), + DS3LocationData("PW1: Slave Knight Armor - settlement roofs, drop by ladder", + "Slave Knight Armor"), + DS3LocationData("PW1: Slave Knight Gauntlets - settlement roofs, drop by ladder", + "Slave Knight Gauntlets"), + DS3LocationData("PW1: Slave Knight Leggings - settlement roofs, drop by ladder", + "Slave Knight Leggings"), + DS3LocationData("PW1: Ember - settlement main, left building after bridge", "Ember"), + DS3LocationData("PW1: Dark Gem - settlement back, egg building", "Dark Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement roofs, balcony", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfire", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Rusted Gold Coin - settlement roofs, roof near second ladder", + "Rusted Gold Coin x3"), + DS3LocationData("PW1: Soul of a Crestfallen Knight - settlement hall, rafters", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW1: Way of White Corona - settlement hall, by altar", + "Way of White Corona"), + DS3LocationData("PW1: Rusted Coin - right of library", "Rusted Coin x2"), + DS3LocationData("PW1: Young White Branch - right of library", "Young White Branch"), + DS3LocationData("PW1: Budding Green Blossom - settlement courtyard, ledge", + "Budding Green Blossom x3"), + DS3LocationData("PW1: Crow Talons - settlement roofs, near bonfire", "Crow Talons"), + DS3LocationData("PW1: Hollow Gem - beside chapel", "Hollow Gem"), + DS3LocationData("PW1: Rime-blue Moss Clump - below bridge far", "Rime-blue Moss Clump x4"), + DS3LocationData("PW1: Follower Sabre - roots above depths", "Follower Sabre"), + DS3LocationData("PW1: Ember - roots above depths", "Ember"), + DS3LocationData("PW1: Snap Freeze - depths, far end, mob drop", "Snap Freeze", drop=True, + hidden=True), # Guaranteed drop from normal-looking Tree Woman + DS3LocationData("PW1: Rime-blue Moss Clump - snowfield upper, overhang", + "Rime-blue Moss Clump"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliff", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Ember - settlement, building near bonfire", "Ember"), + DS3LocationData("PW1: Frozen Weapon - snowfield lower, egg zone", "Frozen Weapon"), + DS3LocationData("PW1: Titanite Slab - depths, up secret ladder", "Titanite Slab", + static='11,0:54500640::', + hidden=True), # Must kill normal-looking Tree Woman + DS3LocationData("PW1: Homeward Bone - depths, up hill", "Homeward Bone x2"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - below snowfield village overhang", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Large Soul of a Weary Warrior - settlement hall roof", + "Large Soul of a Weary Warrior"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement back", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Heavy Gem - snowfield village", "Heavy Gem"), + DS3LocationData("PW1: Large Soul of a Weary Warrior - snowfield tower, 6F", + "Large Soul of a Weary Warrior"), + DS3LocationData("PW1: Millwood Battle Axe - snowfield tower, 5F", "Millwood Battle Axe"), + DS3LocationData("PW1: Ethereal Oak Shield - snowfield tower, 3F", "Ethereal Oak Shield"), + DS3LocationData("PW1: Soul of a Weary Warrior - snowfield tower, 1F", + "Soul of a Weary Warrior"), + DS3LocationData("PW1: Twinkling Titanite - snowfield tower, 3F lizard", + "Twinkling Titanite", lizard=True), + DS3LocationData("PW1: Large Titanite Shard - lizard under bridge near", + "Large Titanite Shard", lizard=True), + DS3LocationData("PW1: Twinkling Titanite - roots, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("PW1: Twinkling Titanite - settlement roofs, lizard before hall", + "Twinkling Titanite", lizard=True), + DS3LocationData("PW1: Large Titanite Shard - settlement loop, lizard", + "Large Titanite Shard x2", lizard=True), ], - "Painted World of Ariandel 2": [ - DS3LocationData("PW: Quakestone Hammer", "Quakestone Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Earth Seeker", "Earth Seeker", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Follower Torch", "Follower Torch", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Follower Shield", "Follower Shield", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Follower Sabre", "Follower Sabre", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Snap Freeze", "Snap Freeze", DS3LocationCategory.SPELL), - DS3LocationData("PW: Floating Chaos", "Floating Chaos", DS3LocationCategory.SPELL), - DS3LocationData("PW: Pyromancer's Parting Flame", "Pyromancer's Parting Flame", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Vilhelm's Helm", "Vilhelm's Helm", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Armor", "Vilhelm's Armor", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Gauntlets", "Vilhelm's Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Leggings", "Vilhelm's Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Valorheart", "Valorheart", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Champion's Bones", "Champion's Bones", DS3LocationCategory.MISC), - DS3LocationData("PW: Soul of Sister Friede", "Soul of Sister Friede", DS3LocationCategory.BOSS), - DS3LocationData("PW: Chillbite Ring", "Chillbite Ring", DS3LocationCategory.RING), + "Painted World of Ariandel (After Contraption)": [ + DS3LocationData("PW2: Soul of Sister Friede", "Soul of Sister Friede", prominent=True, + boss=True), + DS3LocationData("PW2: Titanite Slab - boss drop", "Titanite Slab", + static='11,0:50004700::', + boss=True), # One-time drop after Friede Phase 2 + DS3LocationData("PW2: Floating Chaos - NPC drop", "Floating Chaos", hostile_npc=True, + hidden=True), # Livid Pyromancer Dunnel drop (requires ember) + DS3LocationData("PW2: Prism Stone - pass, tree by beginning", "Prism Stone x10"), + DS3LocationData("PW2: Titanite Chunk - pass, cliff overlooking bonfire", "Titanite Chunk"), + DS3LocationData("PW2: Titanite Chunk - pass, by kickable tree", "Titanite Chunk"), + DS3LocationData("PW2: Follower Shield - pass, far cliffside", "Follower Shield"), + DS3LocationData("PW2: Large Titanite Shard - pass, just before B1", + "Large Titanite Shard x2"), + DS3LocationData("PW2: Quakestone Hammer - pass, side path near B1", "Quakestone Hammer"), + DS3LocationData("PW2: Ember - pass, central alcove", "Ember"), + DS3LocationData("PW2: Large Titanite Shard - pass, far side path", + "Large Titanite Shard x2"), + DS3LocationData("PW2: Soul of a Crestfallen Knight - pit edge #1", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Soul of a Crestfallen Knight - pit edge #2", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Large Soul of a Crestfallen Knight - pit, by tree", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Earth Seeker - pit cave", "Earth Seeker"), + DS3LocationData("PW2: Follower Torch - pass, far side path", "Follower Torch"), + DS3LocationData("PW2: Dung Pie - B1", "Dung Pie x2"), + DS3LocationData("PW2: Vilhelm's Helm", "Vilhelm's Helm"), + DS3LocationData("PW2: Vilhelm's Armor - B2, along wall", "Vilhelm's Armor"), + DS3LocationData("PW2: Vilhelm's Gauntlets - B2, along wall", "Vilhelm's Gauntlets"), + DS3LocationData("PW2: Vilhelm's Leggings - B2, along wall", "Vilhelm's Leggings"), + DS3LocationData("PW2: Blood Gem - B2, center", "Blood Gem"), + DS3LocationData("PW2: Pyromancer's Parting Flame - rotunda", + "Pyromancer's Parting Flame", hidden=True), # Behind illusory wall + DS3LocationData("PW2: Homeward Bone - rotunda", "Homeward Bone x2", + hidden=True), # Behind illusory wall + DS3LocationData("PW2: Twinkling Titanite - B3, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("PW2: Twinkling Titanite - B3, lizard #2", "Twinkling Titanite", + lizard=True), + + # Corvian Settler after killing Friede + DS3LocationData("PW1: Titanite Slab - Corvian", "Titanite Slab", npc=True), + + # Shrine Handmaid after killing Sister Friede + DS3LocationData("FS: Ordained Hood - shop after killing PW2 boss", "Ordained Hood", + boss=True, shop=True), + DS3LocationData("FS: Ordained Dress - shop after killing PW2 boss", "Ordained Dress", + boss=True, shop=True), + DS3LocationData("FS: Ordained Trousers - shop after killing PW2 boss", "Ordained Trousers", + boss=True, shop=True), ], "Dreg Heap": [ - DS3LocationData("DH: Loincloth", "Loincloth", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Aquamarine Dagger", "Aquamarine Dagger", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Murky Hand Scythe", "Murky Hand Scythe", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Murky Longstaff", "Murky Longstaff", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Great Soul Dregs", "Great Soul Dregs", DS3LocationCategory.SPELL), - DS3LocationData("DH: Lothric War Banner", "Lothric War Banner", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Projected Heal", "Projected Heal", DS3LocationCategory.SPELL), - DS3LocationData("DH: Desert Pyromancer Hood", "Desert Pyromancer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Garb", "Desert Pyromancer Garb", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Gloves", "Desert Pyromancer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Skirt", "Desert Pyromancer Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Giant Door Shield", "Giant Door Shield", DS3LocationCategory.SHIELD), - DS3LocationData("DH: Herald Curved Greatsword", "Herald Curved Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Flame Fan", "Flame Fan", DS3LocationCategory.SPELL), - DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", DS3LocationCategory.BOSS), - DS3LocationData("DH: Small Envoy Banner", "Small Envoy Banner", DS3LocationCategory.KEY), - DS3LocationData("DH: Ring of Favor+3", "Ring of Favor+3", DS3LocationCategory.RING), - DS3LocationData("DH: Covetous Silver Serpent Ring+3", "Covetous Silver Serpent Ring+3", DS3LocationCategory.RING), - DS3LocationData("DH: Ring of Steel Protection+3", "Ring of Steel Protection+3", DS3LocationCategory.RING), + DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", + prominent=True, boss=True), + DS3LocationData("DH: Siegbräu - Lapp", "Siegbräu", missable=True, drop=True, + npc=True), # Lapp (quest or kill) + DS3LocationData("DH: Flame Fan - swamp upper, NPC drop", "Flame Fan", + hostile_npc=True), # Desert Pyromancer Zoey drop + DS3LocationData("DH: Ember - castle, behind spire", "Ember"), + DS3LocationData("DH: Soul of a Weary Warrior - castle overhang", "Soul of a Weary Warrior"), + DS3LocationData("DH: Titanite Chunk - castle, up stairs", "Titanite Chunk"), + DS3LocationData("DH: Aquamarine Dagger - castle, up stairs", "Aquamarine Dagger"), + DS3LocationData("DH: Twinkling Titanite - library, chandelier", "Twinkling Titanite"), + DS3LocationData("DH: Murky Hand Scythe - library, behind bookshelves", "Murky Hand Scythe"), + DS3LocationData("DH: Divine Blessing - library, after drop", "Divine Blessing"), + DS3LocationData("DH: Ring of Steel Protection+3 - ledge before church", + "Ring of Steel Protection+3"), + DS3LocationData("DH: Soul of a Crestfallen Knight - church, altar", + "Soul of a Crestfallen Knight"), + DS3LocationData("DH: Rusted Coin - behind fountain after church", "Rusted Coin x2"), + DS3LocationData("DH: Titanite Chunk - pantry, first room", "Titanite Chunk"), + DS3LocationData("DH: Murky Longstaff - pantry, last room", "Murky Longstaff"), + DS3LocationData("DH: Ember - pantry, behind crates just before upstairs", "Ember", + hidden=True), # Behind illusory wall + DS3LocationData("DH: Great Soul Dregs - pantry upstairs", "Great Soul Dregs", + hidden=True), # Behind illusory wall + DS3LocationData("DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop down", + "Covetous Silver Serpent Ring+3", hidden=True), # Behind illusory wall + DS3LocationData("DH: Titanite Chunk - path from church, by pillar", "Titanite Chunk"), + DS3LocationData("DH: Homeward Bone - end of path from church", "Homeward Bone x3"), + DS3LocationData("DH: Lightning Urn - wall outside church", "Lightning Urn x4"), + DS3LocationData("DH: Projected Heal - parapets balcony", "Projected Heal"), + DS3LocationData("DH: Large Soul of a Weary Warrior - parapets, hall", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Lothric War Banner - parapets, end of hall", "Lothric War Banner"), + DS3LocationData("DH: Titanite Scale - library, back of room", "Titanite Scale"), + DS3LocationData("DH: Black Firebomb - ruins, up windmill from bonfire", "Black Firebomb x4"), + DS3LocationData("DH: Titanite Chunk - ruins, path from bonfire", "Titanite Chunk"), + DS3LocationData("DH: Twinkling Titanite - ruins, root near bonfire", "Twinkling Titanite"), + DS3LocationData("DH: Desert Pyromancer Garb - ruins, by shack near cliff", + "Desert Pyromancer Garb"), + DS3LocationData("DH: Titanite Chunk - ruins, by far shack", "Titanite Chunk x2"), + DS3LocationData("DH: Giant Door Shield - ruins, path below far shack", "Giant Door Shield"), + DS3LocationData("DH: Ember - ruins, alcove before swamp", "Ember"), + DS3LocationData("DH: Desert Pyromancer Gloves - swamp, far right", + "Desert Pyromancer Gloves"), + DS3LocationData("DH: Desert Pyromancer Skirt - swamp right, by roots", + "Desert Pyromancer Skirt"), + DS3LocationData("DH: Titanite Scale - swamp upper, drop and jump into tower", + "Titanite Scale"), + DS3LocationData("DH: Purple Moss Clump - swamp shack", "Purple Moss Clump x4"), + DS3LocationData("DH: Ring of Favor+3 - swamp right, up root", "Ring of Favor+3"), + DS3LocationData("DH: Titanite Chunk - swamp right, drop partway up root", "Titanite Chunk"), + DS3LocationData("DH: Large Soul of a Weary Warrior - swamp, under overhang", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Titanite Slab - swamp, path under overhang", "Titanite Slab"), + DS3LocationData("DH: Titanite Chunk - swamp, along buildings", "Titanite Chunk"), + DS3LocationData("DH: Loincloth - swamp, left edge", "Loincloth"), + DS3LocationData("DH: Titanite Chunk - swamp, path to upper", "Titanite Chunk"), + DS3LocationData("DH: Large Soul of a Weary Warrior - swamp center", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Harald Curved Greatsword - swamp left, under root", + "Harald Curved Greatsword"), + DS3LocationData("DH: Homeward Bone - swamp left, on root", "Homeward Bone"), + DS3LocationData("DH: Prism Stone - swamp upper, tunnel start", "Prism Stone x6"), + DS3LocationData("DH: Desert Pyromancer Hood - swamp upper, tunnel end", + "Desert Pyromancer Hood"), + DS3LocationData("DH: Twinkling Titanite - swamp upper, drop onto root", + "Twinkling Titanite", hidden=True), # Hidden fall + DS3LocationData("DH: Divine Blessing - swamp upper, building roof", "Divine Blessing"), + DS3LocationData("DH: Ember - ruins, alcove on cliff", "Ember", hidden=True), # Hidden fall + DS3LocationData("DH: Small Envoy Banner - boss drop", "Small Envoy Banner", + progression=True, boss=True), + DS3LocationData("DH: Twinkling Titanite - ruins, alcove on cliff, mob drop", + "Twinkling Titanite x2", drop=True, + hidden=True), # Hidden fall, also guaranteed drop from killing normal-looking pilgrim + DS3LocationData("DH: Twinkling Titanite - swamp upper, mob drop on roof", + "Twinkling Titanite x2", drop=True, + hidden=True), # Hidden fall, also guaranteed drop from killing normal-looking pilgrim + DS3LocationData("DH: Twinkling Titanite - path after church, mob drop", + "Twinkling Titanite x2", drop=True, + hidden=True), # Guaranteed drop from killing normal-looking pilgrim + + # Stone-humped Hag's shop + DS3LocationData("DH: Splitleaf Greatsword - shop", "Splitleaf Greatsword", shop=True), + DS3LocationData("DH: Divine Blessing - shop", "Divine Blessing", shop=True), + DS3LocationData("DH: Hidden Blessing - shop", "Hidden Blessing", shop=True), + DS3LocationData("DH: Rusted Gold Coin - shop", "Rusted Gold Coin", shop=True), + DS3LocationData("DH: Ember - shop", "Ember", shop=True), ], "Ringed City": [ - DS3LocationData("RC: Ruin Sentinel Helm", "Ruin Sentinel Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Armor", "Ruin Sentinel Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Gauntlets", "Ruin Sentinel Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Leggings", "Ruin Sentinel Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Veil", "Black Witch Veil", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Hat", "Black Witch Hat", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Garb", "Black Witch Garb", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Wrappings", "Black Witch Wrappings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Trousers", "Black Witch Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RC: White Preacher Head", "White Preacher Head", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Havel's Ring+3", "Havel's Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Ringed Knight Spear", "Ringed Knight Spear", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Dragonhead Shield", "Dragonhead Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RC: Ringed Knight Straight Sword", "Ringed Knight Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Preacher's Right Arm", "Preacher's Right Arm", DS3LocationCategory.WEAPON), - DS3LocationData("RC: White Birch Bow", "White Birch Bow", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Church Guardian Shiv", "Church Guardian Shiv", DS3LocationCategory.MISC), - DS3LocationData("RC: Dragonhead Greatshield", "Dragonhead Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("RC: Ringed Knight Paired Greatswords", "Ringed Knight Paired Greatswords", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Shira's Crown", "Shira's Crown", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Armor", "Shira's Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Gloves", "Shira's Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Trousers", "Shira's Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Crucifix of the Mad King", "Crucifix of the Mad King", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Sacred Chime of Filianore", "Sacred Chime of Filianore", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Iron Dragonslayer Helm", "Iron Dragonslayer Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Armor", "Iron Dragonslayer Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Gauntlets", "Iron Dragonslayer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Leggings", "Iron Dragonslayer Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Lightning Arrow", "Lightning Arrow", DS3LocationCategory.SPELL), - DS3LocationData("RC: Ritual Spear Fragment", "Ritual Spear Fragment", DS3LocationCategory.MISC), - DS3LocationData("RC: Antiquated Plain Garb", "Antiquated Plain Garb", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Violet Wrappings", "Violet Wrappings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", DS3LocationCategory.BOSS), - DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", DS3LocationCategory.BOSS), - DS3LocationData("RC: Blood of the Dark Soul", "Blood of the Dark Soul", DS3LocationCategory.KEY), - DS3LocationData("RC: Chloranthy Ring+3", "Chloranthy Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Covetous Gold Serpent Ring+3", "Covetous Gold Serpent Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Ring of the Evil Eye+3", "Ring of the Evil Eye+3", DS3LocationCategory.RING), - DS3LocationData("RC: Wolf Ring+3", "Wolf Ring+3", DS3LocationCategory.RING), + DS3LocationData("RC: Titanite Slab - mid boss drop", "Titanite Slab", + prominent=True, boss=True), # Halflight drop, only once + DS3LocationData("RC: Filianore's Spear Ornament - mid boss drop", + "Filianore's Spear Ornament"), + DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", prominent=True, + boss=True), + DS3LocationData("RC: Sacred Chime of Filianore - ashes, NPC drop", + "Sacred Chime of Filianore", + hostile_npc=True), # Shira (kill or quest) + DS3LocationData("RC: Titanite Slab - ashes, NPC drop", "Titanite Slab", + hostile_npc=True), # Shira (kill or quest) + DS3LocationData("RC: Crucifix of the Mad King - ashes, NPC drop", + "Crucifix of the Mad King", hostile_npc=True), # Shira drop + DS3LocationData("RC: Ledo's Great Hammer - streets high, opposite building, NPC drop", + "Ledo's Great Hammer", hostile_npc=True, + missable=True), # Silver Knight Ledo drop, doesn't invade once Halflight + # is defeated + DS3LocationData("RC: Wolf Ring+3 - street gardens, NPC drop", "Wolf Ring+3", + hostile_npc=True, + missable=True), # Alva drop, doesn't invade once Halflight is defeated + DS3LocationData("RC: Blindfold Mask - grave, NPC drop", "Blindfold Mask", + hostile_npc=True), # Moaning Knight drop + DS3LocationData("RC: Titanite Scale - wall top, behind spawn", "Titanite Scale"), # wrong + DS3LocationData("RC: Ruin Helm - wall top, under stairs to bonfire", "Ruin Helm"), + DS3LocationData("RC: Ruin Armor - wall top, under stairs to bonfire", "Ruin Armor"), + DS3LocationData("RC: Ruin Gauntlets - wall top, under stairs to bonfire", "Ruin Gauntlets"), + DS3LocationData("RC: Ruin Leggings - wall top, under stairs to bonfire", "Ruin Leggings"), + DS3LocationData("RC: Budding Green Blossom - wall top, in flower cluster", + "Budding Green Blossom x2"), + DS3LocationData("RC: Titanite Chunk - wall top, among graves", "Titanite Chunk x2"), + DS3LocationData("RC: Ember - wall top, by statue", "Ember"), + DS3LocationData("RC: Budding Green Blossom - wall top, flowers by stairs", + "Budding Green Blossom x2"), + DS3LocationData("RC: Hidden Blessing - wall top, tomb under platform", "Hidden Blessing", + hidden=True), # hidden fall + DS3LocationData("RC: Soul of a Crestfallen Knight - wall top, under drop", + "Soul of a Crestfallen Knight", hidden=True), # hidden fall + DS3LocationData("RC: Large Soul of a Weary Warrior - wall top, right of small tomb", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Ember - wall upper, balcony", "Ember"), + DS3LocationData("RC: Purging Stone - wall top, by door to upper", "Purging Stone x2"), + DS3LocationData("RC: Hollow Gem - wall upper, path to tower", "Hollow Gem"), + DS3LocationData("RC: Titanite Chunk - wall upper, courtyard alcove", "Titanite Chunk"), + DS3LocationData("RC: Twinkling Titanite - wall tower, jump from chandelier", + "Twinkling Titanite", hidden=True), # Hidden fall + DS3LocationData("RC: Shriving Stone - wall tower, bottom floor center", "Shriving Stone"), + DS3LocationData("RC: Shira's Crown - Shira's room after killing ashes NPC", "Shira's Crown", + hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Armor - Shira's room after killing ashes NPC", "Shira's Armor", + hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Gloves - Shira's room after killing ashes NPC", + "Shira's Gloves", hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Trousers - Shira's room after killing ashes NPC", + "Shira's Trousers", hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Mossfruit - streets near left, path to garden", "Mossfruit x2"), + DS3LocationData("RC: Large Soul of a Crestfallen Knight - streets, far stairs", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("RC: Ringed Knight Spear - streets, down far right hall", + "Ringed Knight Spear"), + DS3LocationData("RC: Black Witch Hat - streets garden", "Black Witch Hat", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Garb - streets garden", "Black Witch Garb", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Wrappings - streets garden", "Black Witch Wrappings", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Trousers - streets garden", "Black Witch Trousers", + hostile_npc=True), # Alva + DS3LocationData("RC: Dragonhead Shield - streets monument, across bridge", + "Dragonhead Shield", hidden=True), # "Show Your Humanity" puzzle + DS3LocationData("RC: Titanite Chunk - streets, near left drop", "Titanite Chunk", + hidden=True), # Hidden fall + DS3LocationData("RC: Mossfruit - streets, far left alcove", "Mossfruit x2"), + DS3LocationData("RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "Large Soul of a Crestfallen Knight", + hidden=True), # "Show Your Humanity" puzzle + DS3LocationData("RC: Covetous Gold Serpent Ring+3 - streets, by Lapp", + "Covetous Gold Serpent Ring+3"), + DS3LocationData("RC: Titanite Chunk - streets high, building opposite", "Titanite Chunk x2"), + DS3LocationData("RC: Dark Gem - swamp near, by stairs", "Dark Gem"), + DS3LocationData("RC: Prism Stone - swamp near, railing by bonfire", "Prism Stone x4"), + DS3LocationData("RC: Ringed Knight Straight Sword - swamp near, tower on peninsula", + "Ringed Knight Straight Sword"), + DS3LocationData("RC: Havel's Ring+3 - streets high, drop from building opposite", + "Havel's Ring+3", hidden=True), # Hidden fall + DS3LocationData("RC: Titanite Chunk - swamp near left, by spire top", "Titanite Chunk"), + DS3LocationData("RC: Twinkling Titanite - swamp near left", "Twinkling Titanite"), + DS3LocationData("RC: Soul of a Weary Warrior - swamp center", "Soul of a Weary Warrior"), + DS3LocationData("RC: Preacher's Right Arm - swamp near right, by tower", + "Preacher's Right Arm"), + DS3LocationData("RC: Rubbish - swamp far, by crystal", "Rubbish"), + DS3LocationData("RC: Titanite Chunk - swamp near right, behind rock", + "Titanite Chunk"), + DS3LocationData("RC: Black Witch Veil - swamp near right, by sunken church", + "Black Witch Veil"), + DS3LocationData("RC: Twinkling Titanite - swamp near right, on sunken church", + "Twinkling Titanite"), + DS3LocationData("RC: Soul of a Crestfallen Knight - swamp near left, nook", + "Soul of a Crestfallen Knight"), + DS3LocationData("RC: White Preacher Head - swamp near, nook right of stairs", + "White Preacher Head"), + DS3LocationData("RC: Titanite Scale - swamp far, by miniboss", "Titanite Scale"), + DS3LocationData("RC: Dragonhead Greatshield - lower cliff, under bridge", + "Dragonhead Greatshield"), + DS3LocationData("RC: Titanite Scale - lower cliff, path under bridge", "Titanite Scale x2"), + DS3LocationData("RC: Rubbish - lower cliff, middle", "Rubbish"), + DS3LocationData("RC: Large Soul of a Weary Warrior - lower cliff, end", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Titanite Scale - lower cliff, first alcove", "Titanite Scale x2"), + DS3LocationData("RC: Titanite Scale - lower cliff, lower path", "Titanite Scale"), + DS3LocationData("RC: Lightning Gem - grave, room after first drop", "Lightning Gem"), + DS3LocationData("RC: Blessed Gem - grave, down lowest stairs", "Blessed Gem"), + DS3LocationData("RC: Simple Gem - grave, up stairs after first drop", "Simple Gem"), + DS3LocationData("RC: Large Soul of a Weary Warrior - wall lower, past two illusory walls", + "Large Soul of a Weary Warrior", hidden=True), + DS3LocationData("RC: Lightning Arrow - wall lower, past three illusory walls", + "Lightning Arrow"), + DS3LocationData("RC: Chloranthy Ring+3 - wall hidden, drop onto statue", + "Chloranthy Ring+3", hidden=True), # Hidden fall + DS3LocationData("RC: Ember - wall hidden, statue room", "Ember"), + DS3LocationData("RC: Filianore's Spear Ornament - wall hidden, by ladder", + "Filianore's Spear Ornament"), + DS3LocationData("RC: Antiquated Plain Garb - wall hidden, before boss", + "Antiquated Plain Garb"), + DS3LocationData("RC: Violet Wrappings - wall hidden, before boss", "Violet Wrappings"), + DS3LocationData("RC: Soul of a Weary Warrior - lower cliff, by first alcove", + "Soul of a Weary Warrior"), + DS3LocationData("RC: Twinkling Titanite - church path, left of boss door", + "Twinkling Titanite x2"), + DS3LocationData("RC: Budding Green Blossom - church path", "Budding Green Blossom x3"), + DS3LocationData("RC: Titanite Chunk - swamp center, peninsula edge", "Titanite Chunk"), + DS3LocationData("RC: Large Soul of a Weary Warrior - swamp center, by peninsula", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Soul of a Weary Warrior - swamp right, by sunken church", + "Soul of a Weary Warrior"), + DS3LocationData("RC: Titanite Scale - upper cliff, bridge", "Titanite Scale"), + DS3LocationData("RC: Soul of a Crestfallen Knight - swamp far, behind crystal", + "Soul of a Crestfallen Knight"), + DS3LocationData("RC: White Birch Bow - swamp far left, up hill", "White Birch Bow"), + DS3LocationData("RC: Titanite Chunk - swamp far left, up hill", "Titanite Chunk"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #1", + "Young White Branch"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #2", + "Young White Branch"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #3", + "Young White Branch"), + DS3LocationData("RC: Ringed Knight Paired Greatswords - church path, mob drop", + "Ringed Knight Paired Greatswords", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Ringed Knight + DS3LocationData("RC: Hidden Blessing - swamp center, mob drop", "Hidden Blessing", + drop=True, hidden=True), # Guaranteed drop from Judicator + DS3LocationData("RC: Divine Blessing - wall top, mob drop", "Divine Blessing", + drop=True, hidden=True), # Guaranteed drop from Judicator + DS3LocationData("RC: Divine Blessing - streets monument, mob drop", "Divine Blessing", + drop=True, + hidden=True), # Guaranteed drop from Judicator, "Show Your Humanity" puzzle + DS3LocationData("RC: Ring of the Evil Eye+3 - grave, mimic", "Ring of the Evil Eye+3", + mimic=True), + DS3LocationData("RC: Iron Dragonslayer Helm - swamp far, miniboss drop", + "Iron Dragonslayer Helm", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Armor - swamp far, miniboss drop", + "Iron Dragonslayer Armor", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Gauntlets - swamp far, miniboss drop", + "Iron Dragonslayer Gauntlets", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Leggings - swamp far, miniboss drop", + "Iron Dragonslayer Leggings", miniboss=True), + DS3LocationData("RC: Church Guardian Shiv - swamp far left, in building", + "Church Guardian Shiv"), + DS3LocationData("RC: Spears of the Church - hidden boss drop", "Spears of the Church", + boss=True), # Midir drop + DS3LocationData("RC: Ritual Spear Fragment - church path", "Ritual Spear Fragment"), + DS3LocationData("RC: Titanite Scale - swamp far, lagoon entrance", "Titanite Scale"), + DS3LocationData("RC: Twinkling Titanite - grave, lizard past first drop", + "Twinkling Titanite", lizard=True), + DS3LocationData("RC: Titanite Scale - grave, lizard past first drop", "Titanite Scale", + lizard=True), + DS3LocationData("RC: Twinkling Titanite - streets high, lizard", "Twinkling Titanite x2", + lizard=True), + DS3LocationData("RC: Titanite Scale - wall lower, lizard", "Titanite Scale", lizard=True), + DS3LocationData("RC: Twinkling Titanite - wall top, lizard on side path", + "Twinkling Titanite", lizard=True), + DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", + prominent=True, boss=True), + DS3LocationData("RC: Blood of the Dark Soul - end boss drop", "Blood of the Dark Soul"), + DS3LocationData("RC: Titanite Slab - ashes, mob drop", "Titanite Slab", + drop=True, + hidden=True), # Guaranteed drop from normal-looking Ringed Knight + + # Lapp + DS3LocationData("RC: Siegbräu - Lapp", "Siegbräu", missable=True, + npc=True), # Lapp (quest) + # Quest or Shrine Handmaiden after death + DS3LocationData("RC: Lapp's Helm - Lapp", "Lapp's Helm", npc=True, shop=True), + DS3LocationData("RC: Lapp's Armor - Lapp", "Lapp's Armor", npc=True, shop=True), + DS3LocationData("RC: Lapp's Gauntlets - Lapp", "Lapp's Gauntlets", npc=True, shop=True), + DS3LocationData("RC: Lapp's Leggings - Lapp", "Lapp's Leggings", npc=True, shop=True), + ], + + # Unlockable shops. We only bother creating a "region" for these for shops that are locked + # behind keys and always have items available either through the shop or through the NPC's + # ashes. + "Greirat's Shop": [ + DS3LocationData("FS: Blue Tearstone Ring - Greirat", "Blue Tearstone Ring", + static='01,0:50006120::', npc=True), + DS3LocationData("FS: Ember - Greirat", "Ember", static="99,0:-1:110000,120000,70000110:", + shop=True, npc=True), + + # Undead Settlement rewards + DS3LocationData("FS: Divine Blessing - Greirat from US", "Divine Blessing", + static='99,0:-1:110000,120000,70000150,70000175:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Ember - Greirat from US", "Ember", + static='99,0:-1:110000,120000,70000150,70000175:', missable=True, + shop=True, npc=True), + + # Irityhll rewards + DS3LocationData("FS: Divine Blessing - Greirat from IBV", "Divine Blessing", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Hidden Blessing - Greirat from IBV", "Hidden Blessing", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Titanite Scale - Greirat from IBV", "Titanite Scale", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Twinkling Titanite - Greirat from IBV", "Twinkling Titanite", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + + # Lothric rewards (from Shrine Handmaid) + DS3LocationData("FS: Ember - shop for Greirat's Ashes", "Twinkling Titanite", + static='99,0:-1:110000,120000,70000152,70000177:', missable=True, + shop=True, npc=True), + ], + "Karla's Shop": [ + DS3LocationData("FS: Affinity - Karla", "Affinity", shop=True, npc=True), + DS3LocationData("FS: Dark Edge - Karla", "Dark Edge", shop=True, npc=True), + + # Quelana Pyromancy Tome + DS3LocationData("FS: Firestorm - Karla for Quelana Tome", "Firestorm", missable=True, + shop=True, npc=True), + DS3LocationData("FS: Rapport - Karla for Quelana Tome", "Rapport", missable=True, + shop=True, npc=True), + DS3LocationData("FS: Fire Whip - Karla for Quelana Tome", "Fire Whip", missable=True, + shop=True, npc=True), + + # Grave Warden Pyromancy Tome + DS3LocationData("FS: Black Flame - Karla for Grave Warden Tome", "Black Flame", + missable=True, shop=True, npc=True), + DS3LocationData("FS: Black Fire Orb - Karla for Grave Warden Tome", "Black Fire Orb", + missable=True, shop=True, npc=True), + + # Deep Braille Divine Tome. This can also be given to Irina, but it'll fail her quest + DS3LocationData("FS: Gnaw - Karla for Deep Braille Tome", "Gnaw", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Deep Protection - Karla for Deep Braille Tome", "Deep Protection", + missable=True, npc=True, shop=True), + + # Londor Braille Divine Tome. This can also be given to Irina, but it'll fail her quest + DS3LocationData("FS: Vow of Silence - Karla for Londor Tome", "Vow of Silence", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Dark Blade - Karla for Londor Tome", "Dark Blade", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Dead Again - Karla for Londor Tome", "Dead Again", missable=True, + npc=True, shop=True), + + # Drops on death. Missable because the player would have to decide between killing her or + # seeing everything she sells. + DS3LocationData("FS: Karla's Pointed Hat - kill Karla", "Karla's Pointed Hat", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Coat - kill Karla", "Karla's Coat", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Gloves - kill Karla", "Karla's Gloves", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Trousers - kill Karla", "Karla's Trousers", + static='07,0:50006150::', missable=True, drop=True, npc=True), ], +} + +for i, region in enumerate(region_order): + for location in location_tables[region]: location.region_value = i + +for region in [ + "Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)", + "Dreg Heap", + "Ringed City", +]: + for location in location_tables[region]: + location.dlc = True - # Progressive - "Progressive Items 1": [] + - # Upgrade materials - [DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] + - [DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] + - [DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)], - - "Progressive Items 2": [] + - # Items - [DS3LocationData(f"Green Blossom #{i + 1}", "Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Firebomb #{i + 1}", "Firebomb", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Alluring Skull #{i + 1}", "Alluring Skull", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Undead Hunter Charm #{i + 1}", "Undead Hunter Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Duel Charm #{i + 1}", "Duel Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Throwing Knife #{i + 1}", "Throwing Knife", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Gold Pine Resin #{i + 1}", "Gold Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Charcoal Pine Resin #{i + 1}", "Charcoal Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Human Pine Resin #{i + 1}", "Human Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Carthus Rouge #{i + 1}", "Carthus Rouge", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Pale Pine Resin #{i + 1}", "Pale Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Charcoal Pine Bundle #{i + 1}", "Charcoal Pine Bundle", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rotten Pine Resin #{i + 1}", "Rotten Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Homeward Bone #{i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(16)] + - [DS3LocationData(f"Pale Tongue #{i + 1}", "Pale Tongue", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Rusted Coin #{i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rusted Gold Coin #{i + 1}", "Rusted Gold Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Ember #{i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(45)], - - "Progressive Items 3": [] + - # Souls & Bulk Upgrade Materials - [DS3LocationData(f"Fading Soul #{i + 1}", "Fading Soul", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of a Deserted Corpse #{i + 1}", "Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of a Deserted Corpse #{i + 1}", "Large Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of an Unknown Traveler #{i + 1}", "Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of an Unknown Traveler #{i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of a Nameless Soldier #{i + 1}", "Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Large Soul of a Nameless Soldier #{i + 1}", "Large Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Soul of a Weary Warrior #{i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Soul of a Crestfallen Knight #{i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Titanite Chunk #{i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(22)] + - [DS3LocationData(f"Titanite Scale #{i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(29)], - - "Progressive Items 4": [] + - # Gems & Random Consumables - [DS3LocationData(f"Ring of Sacrifice #{i + 1}", "Ring of Sacrifice", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Divine Blessing #{i + 1}", "Divine Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Hidden Blessing #{i + 1}", "Hidden Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Budding Green Blossom #{i + 1}", "Budding Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Bloodred Moss Clump #{i + 1}", "Bloodred Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Purple Moss Clump #{i + 1}", "Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Blooming Purple Moss Clump #{i + 1}", "Blooming Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Purging Stone #{i + 1}", "Purging Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rime-blue Moss Clump #{i + 1}", "Rime-blue Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Repair Powder #{i + 1}", "Repair Powder", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Kukri #{i + 1}", "Kukri", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Lightning Urn #{i + 1}", "Lightning Urn", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Rubbish #{i + 1}", "Rubbish", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Blue Bug Pellet #{i + 1}", "Blue Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Red Bug Pellet #{i + 1}", "Red Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Yellow Bug Pellet #{i + 1}", "Yellow Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Black Bug Pellet #{i + 1}", "Black Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Heavy Gem #{i + 1}", "Heavy Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Sharp Gem #{i + 1}", "Sharp Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Refined Gem #{i + 1}", "Refined Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Crystal Gem #{i + 1}", "Crystal Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Simple Gem #{i + 1}", "Simple Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Fire Gem #{i + 1}", "Fire Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Chaos Gem #{i + 1}", "Chaos Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Lightning Gem #{i + 1}", "Lightning Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Deep Gem #{i + 1}", "Deep Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Dark Gem #{i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Poison Gem #{i + 1}", "Poison Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blood Gem #{i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Raw Gem #{i + 1}", "Raw Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blessed Gem #{i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Hollow Gem #{i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Shriving Stone #{i + 1}", "Shriving Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)], - - "Progressive Items DLC": [] + - # Upgrade materials - [DS3LocationData(f"Large Titanite Shard ${i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Titanite Chunk ${i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] + - [DS3LocationData(f"Titanite Slab ${i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Twinkling Titanite ${i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Titanite Scale ${i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(11)] + - - - # Items - [DS3LocationData(f"Homeward Bone ${i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Rusted Coin ${i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Ember ${i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(10)] + - - # Souls - [DS3LocationData(f"Large Soul of an Unknown Traveler ${i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(9)] + - [DS3LocationData(f"Soul of a Weary Warrior ${i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of a Weary Warrior ${i + 1}", "Large Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Soul of a Crestfallen Knight ${i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Large Soul of a Crestfallen Knight ${i + 1}", "Large Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - - # Gems - [DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)], - - "Progressive Items Health": [] + - # Healing - [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] + - [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)], +for region in [ + "Firelink Shrine Bell Tower", + "Greirat's Shop", + "Karla's Shop" +]: + for location in location_tables[region]: + location.conditional = True + +location_name_groups: Dict[str, Set[str]] = { + # We could insert these locations automatically with setdefault(), but we set them up explicitly + # instead so we can choose the ordering. + "Prominent": set(), + "Progression": set(), + "Boss Rewards": set(), + "Miniboss Rewards": set(), + "Mimic Rewards": set(), + "Hostile NPC Rewards": set(), + "Friendly NPC Rewards": set(), + "Small Crystal Lizards": set(), + "Upgrade": set(), + "Small Souls": set(), + "Boss Souls": set(), + "Unique": set(), + "Healing": set(), + "Miscellaneous": set(), + "Hidden": set(), + "Weapons": set(), + "Shields": set(), + "Armor": set(), + "Rings": set(), + "Spells": set(), +} + +location_descriptions = { + "Prominent": "A small number of locations that are in very obvious locations. Mostly boss " + \ + "drops. Ideal for setting as priority locations.", + "Progression": "Locations that contain items in vanilla which unlock other locations.", + "Boss Rewards": "Boss drops. Does not include soul transfusions or shop items.", + "Miniboss Rewards": "Miniboss drops. Only includes enemies considered minibosses by the " + \ + "enemy randomizer.", + "Mimic Rewards": "Drops from enemies that are mimics in vanilla.", + "Hostile NPC Rewards": "Drops from NPCs that are hostile to you. This includes scripted " + \ + "invaders and initially-friendly NPCs that must be fought as part of their quest.", + "Friendly NPC Rewards": "Items given by friendly NPCs as part of their quests or from " + \ + "non-violent interaction.", + "Upgrade": "Locations that contain upgrade items in vanilla, including titanite, gems, and " + \ + "Shriving Stones.", + "Small Souls": "Locations that contain soul items in vanilla, not including boss souls.", + "Boss Souls": "Locations that contain boss souls in vanilla, as well as Soul of Rosaria.", + "Unique": "Locations that contain items in vanilla that are unique per NG cycle, such as " + \ + "scrolls, keys, ashes, and so on. Doesn't cover equipment, spells, or souls.", + "Healing": "Locations that contain Undead Bone Shards and Estus Shards in vanilla.", + "Miscellaneous": "Locations that contain generic stackable items in vanilla, such as arrows, " + + "firebombs, buffs, and so on.", + "Hidden": "Locations that are particularly difficult to find, such as behind illusory " + \ + "walls, down hidden drops, and so on. Does not include large locations like Untended " + \ + "Graves or Archdragon Peak.", + "Weapons": "Locations that contain weapons in vanilla.", + "Shields": "Locations that contain shields in vanilla.", + "Armor": "Locations that contain armor in vanilla.", + "Rings": "Locations that contain rings in vanilla.", + "Spells": "Locations that contain spells in vanilla.", } location_dictionary: Dict[str, DS3LocationData] = {} -for location_table in location_tables.values(): +for location_name, location_table in location_tables.items(): location_dictionary.update({location_data.name: location_data for location_data in location_table}) + + for location_data in location_table: + if not location_data.is_event: + for group_name in location_data.location_groups(): + location_name_groups[group_name].add(location_data.name) + + # Allow entire locations to be added to location sets. + if not location_name.endswith(" Shop"): + location_name_groups[location_name] = set([ + location_data.name for location_data in location_table + if not location_data.is_event + ]) + +location_name_groups["Painted World of Ariandel"] = ( + location_name_groups["Painted World of Ariandel (Before Contraption)"] + .union(location_name_groups["Painted World of Ariandel (After Contraption)"]) +) +del location_name_groups["Painted World of Ariandel (Before Contraption)"] +del location_name_groups["Painted World of Ariandel (After Contraption)"] + +location_name_groups["DLC"] = ( + location_name_groups["Painted World of Ariandel"] + .union(location_name_groups["Dreg Heap"]) + .union(location_name_groups["Ringed City"]) +) diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index df0bb953b8d9..ad81dd9f7b85 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -1,80 +1,78 @@ -import typing +from dataclasses import dataclass +import json +from typing import Any, Dict -from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink +from Options import Choice, DeathLink, DefaultOnToggle, ExcludeLocations, NamedRange, OptionDict, \ + OptionGroup, PerGameCommonOptions, Range, Removed, Toggle +## Game Options -class RandomizeWeaponLocations(DefaultOnToggle): - """Randomizes weapons (+76 locations)""" - display_name = "Randomize Weapon Locations" - -class RandomizeShieldLocations(DefaultOnToggle): - """Randomizes shields (+24 locations)""" - display_name = "Randomize Shield Locations" - - -class RandomizeArmorLocations(DefaultOnToggle): - """Randomizes armor pieces (+97 locations)""" - display_name = "Randomize Armor Locations" - - -class RandomizeRingLocations(DefaultOnToggle): - """Randomizes rings (+49 locations)""" - display_name = "Randomize Ring Locations" - - -class RandomizeSpellLocations(DefaultOnToggle): - """Randomizes spells (+18 locations)""" - display_name = "Randomize Spell Locations" - - -class RandomizeKeyLocations(DefaultOnToggle): - """Randomizes items which unlock doors or bypass barriers""" - display_name = "Randomize Key Locations" +class EarlySmallLothricBanner(Choice): + """Force Small Lothric Banner into an early sphere in your world or across all worlds.""" + display_name = "Early Small Lothric Banner" + option_off = 0 + option_early_global = 1 + option_early_local = 2 + default = option_off -class RandomizeBossSoulLocations(DefaultOnToggle): - """Randomizes Boss Souls (+18 Locations)""" - display_name = "Randomize Boss Soul Locations" +class LateBasinOfVowsOption(Choice): + """Guarantee that you don't need to enter Lothric Castle until later in the run. + - **Off:** You may have to enter Lothric Castle and the areas beyond it immediately after High + Wall of Lothric. + - **After Small Lothric Banner:** You may have to enter Lothric Castle after Catacombs of + Carthus. + - **After Small Doll:** You won't have to enter Lothric Castle until after Irithyll of the + Boreal Valley. + """ + display_name = "Late Basin of Vows" + option_off = 0 + alias_false = 0 + option_after_small_lothric_banner = 1 + alias_true = 1 + option_after_small_doll = 2 -class RandomizeNPCLocations(Toggle): - """Randomizes friendly NPC drops (meaning you will probably have to kill them) (+14 locations)""" - display_name = "Randomize NPC Locations" +class LateDLCOption(Choice): + """Guarantee that you don't need to enter the DLC until later in the run. -class RandomizeMiscLocations(Toggle): - """Randomizes miscellaneous items (ashes, tomes, scrolls, etc.) to the pool. (+36 locations)""" - display_name = "Randomize Miscellaneous Locations" + - **Off:** You may have to enter the DLC after Catacombs of Carthus. + - **After Small Doll:** You may have to enter the DLC after Irithyll of the Boreal Valley. + - **After Basin:** You won't have to enter the DLC until after Lothric Castle. + """ + display_name = "Late DLC" + option_off = 0 + alias_false = 0 + option_after_small_doll = 1 + alias_true = 1 + option_after_basin = 2 -class RandomizeHealthLocations(Toggle): - """Randomizes health upgrade items. (+21 locations)""" - display_name = "Randomize Health Upgrade Locations" +class EnableDLCOption(Toggle): + """Include DLC locations, items, and enemies in the randomized pools. + To use this option, you must own both the "Ashes of Ariandel" and the "Ringed City" DLCs. + """ + display_name = "Enable DLC" -class RandomizeProgressiveLocationsOption(Toggle): - """Randomizes upgrade materials and consumables such as the titanite shards, firebombs, resin, etc... - Instead of specific locations, these are progressive, so Titanite Shard #1 is the first titanite shard - you pick up, regardless of whether it's from an enemy drop late in the game or an item on the ground in the - first 5 minutes.""" - display_name = "Randomize Progressive Locations" +class EnableNGPOption(Toggle): + """Include items and locations exclusive to NG+ cycles.""" + display_name = "Enable NG+" -class PoolTypeOption(Choice): - """Changes which non-progression items you add to the pool +## Equipment - Shuffle: Items are picked from the locations being randomized - Various: Items are picked from a list of all items in the game, but are the same type of item they replace""" - display_name = "Pool Type" - option_shuffle = 0 - option_various = 1 +class RandomizeStartingLoadout(DefaultOnToggle): + """Randomizes the equipment characters begin with.""" + display_name = "Randomize Starting Loadout" -class GuaranteedItemsOption(ItemDict): - """Guarantees that the specified items will be in the item pool""" - display_name = "Guaranteed Items" +class RequireOneHandedStartingWeapons(DefaultOnToggle): + """Require starting equipment to be usable one-handed.""" + display_name = "Require One-Handed Starting Weapons" class AutoEquipOption(Toggle): @@ -83,47 +81,56 @@ class AutoEquipOption(Toggle): class LockEquipOption(Toggle): - """Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the - Auto-equip option.""" + """Lock the equipment slots so you cannot change your armor or your left/right weapons. + + Works great with the Auto-equip option. + """ display_name = "Lock Equipment Slots" +class NoEquipLoadOption(Toggle): + """Disable the equip load constraint from the game.""" + display_name = "No Equip Load" + + class NoWeaponRequirementsOption(Toggle): - """Disable the weapon requirements by removing any movement or damage penalties. - Permitting you to use any weapon early""" + """Disable the weapon requirements by removing any movement or damage penalties, permitting you + to use any weapon early. + """ display_name = "No Weapon Requirements" class NoSpellRequirementsOption(Toggle): - """Disable the spell requirements permitting you to use any spell""" + """Disable the spell requirements permitting you to use any spell.""" display_name = "No Spell Requirements" -class NoEquipLoadOption(Toggle): - """Disable the equip load constraint from the game""" - display_name = "No Equip Load" - +## Weapons class RandomizeInfusionOption(Toggle): """Enable this option to infuse a percentage of the pool of weapons and shields.""" display_name = "Randomize Infusion" -class RandomizeInfusionPercentageOption(Range): - """The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled""" +class RandomizeInfusionPercentageOption(NamedRange): + """The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled. + """ display_name = "Percentage of Infused Weapons" range_start = 0 range_end = 100 default = 33 + # 3/155 weapons are infused in the base game, or about 2% + special_range_names = {"similar to base game": 2} class RandomizeWeaponLevelOption(Choice): - """Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and - maximum levels defined. + """Enable this option to upgrade a percentage of the pool of weapons to a random value between + the minimum and maximum levels defined. - All: All weapons are eligible, both basic and epic - Basic: Only weapons that can be upgraded to +10 - Epic: Only weapons that can be upgraded to +5""" + - **All:** All weapons are eligible, both basic and epic + - **Basic:** Only weapons that can be upgraded to +10 + - **Epic:** Only weapons that can be upgraded to +5 + """ display_name = "Randomize Weapon Level" option_none = 0 option_all = 1 @@ -132,7 +139,7 @@ class RandomizeWeaponLevelOption(Choice): class RandomizeWeaponLevelPercentageOption(Range): - """The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled""" + """The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled.""" display_name = "Percentage of Randomized Weapons" range_start = 0 range_end = 100 @@ -140,7 +147,7 @@ class RandomizeWeaponLevelPercentageOption(Range): class MinLevelsIn5WeaponPoolOption(Range): - """The minimum upgraded value of a weapon in the pool of weapons that can only reach +5""" + """The minimum upgraded value of a weapon in the pool of weapons that can only reach +5.""" display_name = "Minimum Level of +5 Weapons" range_start = 0 range_end = 5 @@ -148,7 +155,7 @@ class MinLevelsIn5WeaponPoolOption(Range): class MaxLevelsIn5WeaponPoolOption(Range): - """The maximum upgraded value of a weapon in the pool of weapons that can only reach +5""" + """The maximum upgraded value of a weapon in the pool of weapons that can only reach +5.""" display_name = "Maximum Level of +5 Weapons" range_start = 0 range_end = 5 @@ -156,7 +163,7 @@ class MaxLevelsIn5WeaponPoolOption(Range): class MinLevelsIn10WeaponPoolOption(Range): - """The minimum upgraded value of a weapon in the pool of weapons that can reach +10""" + """The minimum upgraded value of a weapon in the pool of weapons that can reach +10.""" display_name = "Minimum Level of +10 Weapons" range_start = 0 range_end = 10 @@ -164,72 +171,308 @@ class MinLevelsIn10WeaponPoolOption(Range): class MaxLevelsIn10WeaponPoolOption(Range): - """The maximum upgraded value of a weapon in the pool of weapons that can reach +10""" + """The maximum upgraded value of a weapon in the pool of weapons that can reach +10.""" display_name = "Maximum Level of +10 Weapons" range_start = 0 range_end = 10 default = 10 -class EarlySmallLothricBanner(Choice): - """This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or - into an early sphere across all worlds.""" - display_name = "Early Small Lothric Banner" - option_off = 0 - option_early_global = 1 - option_early_local = 2 - default = option_off +## Item Smoothing +class SmoothSoulItemsOption(DefaultOnToggle): + """Distribute soul items in a similar order as the base game. -class LateBasinOfVowsOption(Toggle): - """This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into - Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early, - but you wont have to fight Dancer to find your Small Lothric Banner.""" - display_name = "Late Basin of Vows" + By default, soul items will be distributed totally randomly. If this is set, less valuable soul + items will generally appear in earlier spheres and more valuable ones will generally appear + later. + """ + display_name = "Smooth Soul Items" -class LateDLCOption(Toggle): - """This option makes it so you are guaranteed to find your Small Doll without having to venture off into the DLC, - effectively putting anything in the DLC in logic after finding both Contraption Key and Small Doll, - and being able to get into Irithyll of the Boreal Valley.""" - display_name = "Late DLC" +class SmoothUpgradeItemsOption(DefaultOnToggle): + """Distribute upgrade items in a similar order as the base game. + By default, upgrade items will be distributed totally randomly. If this is set, lower-level + upgrade items will generally appear in earlier spheres and higher-level ones will generally + appear later. + """ + display_name = "Smooth Upgrade Items" -class EnableDLCOption(Toggle): - """To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC""" - display_name = "Enable DLC" +class SmoothUpgradedWeaponsOption(DefaultOnToggle): + """Distribute upgraded weapons in a similar order as the base game. + + By default, upgraded weapons will be distributed totally randomly. If this is set, lower-level + weapons will generally appear in earlier spheres and higher-level ones will generally appear + later. + """ + display_name = "Smooth Upgraded Weapons" + + +### Enemies + +class RandomizeEnemiesOption(DefaultOnToggle): + """Randomize enemy and boss placements.""" + display_name = "Randomize Enemies" + + +class SimpleEarlyBossesOption(DefaultOnToggle): + """Avoid replacing Iudex Gundyr and Vordt with late bosses. + + This excludes all bosses after Dancer of the Boreal Valley from these two boss fights. Disable + it for a chance at a much harder early game. + + This is ignored unless enemies are randomized. + """ + display_name = "Simple Early Bosses" + + +class ScaleEnemiesOption(DefaultOnToggle): + """Scale randomized enemy stats to match the areas in which they appear. + + Disabling this will tend to make the early game much more difficult and the late game much + easier. + + This is ignored unless enemies are randomized. + """ + display_name = "Scale Enemies" + + +class RandomizeMimicsWithEnemiesOption(Toggle): + """Mix Mimics into the main enemy pool. + + If this is enabled, Mimics will be replaced by normal enemies who drop the Mimic rewards on + death, and Mimics will be placed randomly in place of normal enemies. It's recommended to enable + Impatient Mimics as well if you enable this. + + This is ignored unless enemies are randomized. + """ + display_name = "Randomize Mimics With Enemies" + + +class RandomizeSmallCrystalLizardsWithEnemiesOption(Toggle): + """Mix small Crystal Lizards into the main enemy pool. + + If this is enabled, Crystal Lizards will be replaced by normal enemies who drop the Crystal + Lizard rewards on death, and Crystal Lizards will be placed randomly in place of normal enemies. + + This is ignored unless enemies are randomized. + """ + display_name = "Randomize Small Crystal Lizards With Enemies" + + +class ReduceHarmlessEnemiesOption(Toggle): + """Reduce the frequency that "harmless" enemies appear. + + Enable this to add a bit of extra challenge. This severely limits the number of enemies that are + slow to aggro, slow to attack, and do very little damage that appear in the enemy pool. + + This is ignored unless enemies are randomized. + """ + display_name = "Reduce Harmless Enemies" + + +class AllChestsAreMimicsOption(Toggle): + """Replace all chests with mimics that drop the same items. + + If "Randomize Mimics With Enemies" is set, these chests will instead be replaced with random + enemies that drop the same items. + + This is ignored unless enemies are randomized. + """ + display_name = "All Chests Are Mimics" + + +class ImpatientMimicsOption(Toggle): + """Mimics attack as soon as you get close instead of waiting for you to open them. + + This is ignored unless enemies are randomized. + """ + display_name = "Impatient Mimics" + + +class RandomEnemyPresetOption(OptionDict): + """The YAML preset for the static enemy randomizer. + + See the static randomizer documentation in `randomizer\\presets\\README.txt` for details. + Include this as nested YAML. For example: + + .. code-block:: YAML + + random_enemy_preset: + RemoveSource: Ancient Wyvern; Darkeater Midir + DontRandomize: Iudex Gundyr + """ + display_name = "Random Enemy Preset" + supports_weighting = False + default = {} + + valid_keys = ["Description", "RecommendFullRandomization", "RecommendNoEnemyProgression", + "OopsAll", "Boss", "Miniboss", "Basic", "BuffBasicEnemiesAsBosses", + "DontRandomize", "RemoveSource", "Enemies"] + + @classmethod + def get_option_name(cls, value: Dict[str, Any]) -> str: + return json.dumps(value) + + +## Item & Location + +class DS3ExcludeLocations(ExcludeLocations): + """Prevent these locations from having an important item.""" + default = frozenset({"Hidden", "Small Crystal Lizards", "Upgrade", "Small Souls", "Miscellaneous"}) -dark_souls_options: typing.Dict[str, Option] = { - "enable_weapon_locations": RandomizeWeaponLocations, - "enable_shield_locations": RandomizeShieldLocations, - "enable_armor_locations": RandomizeArmorLocations, - "enable_ring_locations": RandomizeRingLocations, - "enable_spell_locations": RandomizeSpellLocations, - "enable_key_locations": RandomizeKeyLocations, - "enable_boss_locations": RandomizeBossSoulLocations, - "enable_npc_locations": RandomizeNPCLocations, - "enable_misc_locations": RandomizeMiscLocations, - "enable_health_upgrade_locations": RandomizeHealthLocations, - "enable_progressive_locations": RandomizeProgressiveLocationsOption, - "pool_type": PoolTypeOption, - "guaranteed_items": GuaranteedItemsOption, - "auto_equip": AutoEquipOption, - "lock_equip": LockEquipOption, - "no_weapon_requirements": NoWeaponRequirementsOption, - "randomize_infusion": RandomizeInfusionOption, - "randomize_infusion_percentage": RandomizeInfusionPercentageOption, - "randomize_weapon_level": RandomizeWeaponLevelOption, - "randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption, - "min_levels_in_5": MinLevelsIn5WeaponPoolOption, - "max_levels_in_5": MaxLevelsIn5WeaponPoolOption, - "min_levels_in_10": MinLevelsIn10WeaponPoolOption, - "max_levels_in_10": MaxLevelsIn10WeaponPoolOption, - "early_banner": EarlySmallLothricBanner, - "late_basin_of_vows": LateBasinOfVowsOption, - "late_dlc": LateDLCOption, - "no_spell_requirements": NoSpellRequirementsOption, - "no_equip_load": NoEquipLoadOption, - "death_link": DeathLink, - "enable_dlc": EnableDLCOption, -} + +class ExcludedLocationBehaviorOption(Choice): + """How to choose items for excluded locations in DS3. + + - **Allow Useful:** Excluded locations can't have progression items, but they can have useful + items. + - **Forbid Useful:** Neither progression items nor useful items can be placed in excluded + locations. + - **Do Not Randomize:** Excluded locations always contain the same item as in vanilla Dark Souls + III. + + A "progression item" is anything that's required to unlock another location in some game. A + "useful item" is something each game defines individually, usually items that are quite + desirable but not strictly necessary. + """ + display_name = "Excluded Locations Behavior" + option_allow_useful = 1 + option_forbid_useful = 2 + option_do_not_randomize = 3 + default = 2 + + +class MissableLocationBehaviorOption(Choice): + """Which items can be placed in locations that can be permanently missed. + + - **Allow Useful:** Missable locations can't have progression items, but they can have useful + items. + - **Forbid Useful:** Neither progression items nor useful items can be placed in missable + locations. + - **Do Not Randomize:** Missable locations always contain the same item as in vanilla Dark Souls + III. + + A "progression item" is anything that's required to unlock another location in some game. A + "useful item" is something each game defines individually, usually items that are quite + desirable but not strictly necessary. + """ + display_name = "Missable Locations Behavior" + option_allow_useful = 1 + option_forbid_useful = 2 + option_do_not_randomize = 3 + default = 2 + + +@dataclass +class DarkSouls3Options(PerGameCommonOptions): + # Game Options + early_banner: EarlySmallLothricBanner + late_basin_of_vows: LateBasinOfVowsOption + late_dlc: LateDLCOption + death_link: DeathLink + enable_dlc: EnableDLCOption + enable_ngp: EnableNGPOption + + # Equipment + random_starting_loadout: RandomizeStartingLoadout + require_one_handed_starting_weapons: RequireOneHandedStartingWeapons + auto_equip: AutoEquipOption + lock_equip: LockEquipOption + no_equip_load: NoEquipLoadOption + no_weapon_requirements: NoWeaponRequirementsOption + no_spell_requirements: NoSpellRequirementsOption + + # Weapons + randomize_infusion: RandomizeInfusionOption + randomize_infusion_percentage: RandomizeInfusionPercentageOption + randomize_weapon_level: RandomizeWeaponLevelOption + randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption + min_levels_in_5: MinLevelsIn5WeaponPoolOption + max_levels_in_5: MaxLevelsIn5WeaponPoolOption + min_levels_in_10: MinLevelsIn10WeaponPoolOption + max_levels_in_10: MaxLevelsIn10WeaponPoolOption + + # Item Smoothing + smooth_soul_items: SmoothSoulItemsOption + smooth_upgrade_items: SmoothUpgradeItemsOption + smooth_upgraded_weapons: SmoothUpgradedWeaponsOption + + # Enemies + randomize_enemies: RandomizeEnemiesOption + simple_early_bosses: SimpleEarlyBossesOption + scale_enemies: ScaleEnemiesOption + randomize_mimics_with_enemies: RandomizeMimicsWithEnemiesOption + randomize_small_crystal_lizards_with_enemies: RandomizeSmallCrystalLizardsWithEnemiesOption + reduce_harmless_enemies: ReduceHarmlessEnemiesOption + all_chests_are_mimics: AllChestsAreMimicsOption + impatient_mimics: ImpatientMimicsOption + random_enemy_preset: RandomEnemyPresetOption + + # Item & Location + exclude_locations: DS3ExcludeLocations + excluded_location_behavior: ExcludedLocationBehaviorOption + missable_location_behavior: MissableLocationBehaviorOption + + # Removed + pool_type: Removed + enable_weapon_locations: Removed + enable_shield_locations: Removed + enable_armor_locations: Removed + enable_ring_locations: Removed + enable_spell_locations: Removed + enable_key_locations: Removed + enable_boss_locations: Removed + enable_npc_locations: Removed + enable_misc_locations: Removed + enable_health_upgrade_locations: Removed + enable_progressive_locations: Removed + guaranteed_items: Removed + excluded_locations: Removed + missable_locations: Removed + + +option_groups = [ + OptionGroup("Equipment", [ + RandomizeStartingLoadout, + RequireOneHandedStartingWeapons, + AutoEquipOption, + LockEquipOption, + NoEquipLoadOption, + NoWeaponRequirementsOption, + NoSpellRequirementsOption, + ]), + OptionGroup("Weapons", [ + RandomizeInfusionOption, + RandomizeInfusionPercentageOption, + RandomizeWeaponLevelOption, + RandomizeWeaponLevelPercentageOption, + MinLevelsIn5WeaponPoolOption, + MaxLevelsIn5WeaponPoolOption, + MinLevelsIn10WeaponPoolOption, + MaxLevelsIn10WeaponPoolOption, + ]), + OptionGroup("Item Smoothing", [ + SmoothSoulItemsOption, + SmoothUpgradeItemsOption, + SmoothUpgradedWeaponsOption, + ]), + OptionGroup("Enemies", [ + RandomizeEnemiesOption, + SimpleEarlyBossesOption, + ScaleEnemiesOption, + RandomizeMimicsWithEnemiesOption, + RandomizeSmallCrystalLizardsWithEnemiesOption, + ReduceHarmlessEnemiesOption, + AllChestsAreMimicsOption, + ImpatientMimicsOption, + RandomEnemyPresetOption, + ]), + OptionGroup("Item & Location Options", [ + DS3ExcludeLocations, + ExcludedLocationBehaviorOption, + MissableLocationBehaviorOption, + ]) +] diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 020010981160..159a870c7658 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1,15 +1,19 @@ # world/dark_souls_3/__init__.py -from typing import Dict, Set, List +from collections.abc import Sequence +from collections import defaultdict +import json +from logging import warning +from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union -from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification -from Options import Toggle +from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld -from worlds.generic.Rules import set_rule, add_rule, add_item_rule +from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule -from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions -from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary -from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options +from .Bosses import DS3BossInfo, all_bosses, default_yhorm_location +from .Items import DarkSouls3Item, DS3ItemData, Infusion, UsefulIf, filler_item_names, item_descriptions, item_dictionary, item_name_groups +from .Locations import DarkSouls3Location, DS3LocationData, location_tables, location_descriptions, location_dictionary, location_name_groups, region_order +from .Options import DarkSouls3Options, option_groups class DarkSouls3Web(WebWorld): @@ -34,91 +38,117 @@ class DarkSouls3Web(WebWorld): ) tutorials = [setup_en, setup_fr] - + option_groups = option_groups item_descriptions = item_descriptions + rich_text_options_doc = True class DarkSouls3World(World): """ Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware. - Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that + Played from a third-person perspective, players have access to various weapons, armour, magic, and consumables that they can use to fight their enemies. """ - game: str = "Dark Souls III" - option_definitions = dark_souls_options - topology_present: bool = True + game = "Dark Souls III" + options: DarkSouls3Options + options_dataclass = DarkSouls3Options web = DarkSouls3Web() base_id = 100000 - enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) - item_name_to_id = DarkSouls3Item.get_name_to_id() - location_name_to_id = DarkSouls3Location.get_name_to_id() - item_name_groups = { - "Cinders": { - "Cinders of a Lord - Abyss Watcher", - "Cinders of a Lord - Aldrich", - "Cinders of a Lord - Yhorm the Giant", - "Cinders of a Lord - Lothric Prince" - } + item_name_to_id = {data.name: data.ap_code for data in item_dictionary.values() if data.ap_code is not None} + location_name_to_id = { + location.name: location.ap_code + for locations in location_tables.values() + for location in locations + if location.ap_code is not None } + location_name_groups = location_name_groups + item_name_groups = item_name_groups + location_descriptions = location_descriptions + item_descriptions = item_descriptions + + yhorm_location: DS3BossInfo = default_yhorm_location + """If enemy randomization is enabled, this is the boss who Yhorm the Giant should replace. + + This is used to determine where the Storm Ruler can be placed. + """ + + all_excluded_locations: Set[str] = set() + """This is the same value as `self.options.exclude_locations.value` initially, but if + `options.exclude_locations` gets cleared due to `excluded_locations: allow_useful` this still + holds the old locations so we can ensure they don't get necessary items. + """ + + local_itempool: List[DarkSouls3Item] = [] + """The pool of all items within this particular world. This is a subset of + `self.multiworld.itempool`.""" def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) - self.locked_items = [] - self.locked_locations = [] - self.main_path_locations = [] - self.enabled_location_categories = set() - - - def generate_early(self): - if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.WEAPON) - if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.SHIELD) - if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.ARMOR) - if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.RING) - if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.SPELL) - if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.NPC) - if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.KEY) - if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global: - self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1 - elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local: - self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1 - if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.BOSS) - if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.MISC) - if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.HEALTH) - if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM) - - - def create_regions(self): - progressive_location_table = [] - if self.multiworld.enable_progressive_locations[self.player]: - progressive_location_table = [] + \ - location_tables["Progressive Items 1"] + \ - location_tables["Progressive Items 2"] + \ - location_tables["Progressive Items 3"] + \ - location_tables["Progressive Items 4"] - - if self.multiworld.enable_dlc[self.player].value: - progressive_location_table += location_tables["Progressive Items DLC"] - - if self.multiworld.enable_health_upgrade_locations[self.player]: - progressive_location_table += location_tables["Progressive Items Health"] - + self.all_excluded_locations = set() + + def generate_early(self) -> None: + self.all_excluded_locations.update(self.options.exclude_locations.value) + + # Inform Universal Tracker where Yhorm is being randomized to. + if hasattr(self.multiworld, "re_gen_passthrough"): + if "Dark Souls III" in self.multiworld.re_gen_passthrough: + if self.multiworld.re_gen_passthrough["Dark Souls III"]["options"]["randomize_enemies"]: + yhorm_data = self.multiworld.re_gen_passthrough["Dark Souls III"]["yhorm"] + for boss in all_bosses: + if yhorm_data.startswith(boss.name): + self.yhorm_location = boss + + # Randomize Yhorm manually so that we know where to place the Storm Ruler. + elif self.options.randomize_enemies: + self.yhorm_location = self.random.choice( + [boss for boss in all_bosses if self._allow_boss_for_yhorm(boss)]) + + # If Yhorm is early, make sure the Storm Ruler is easily available to avoid BK + # Iudex Gundyr is handled separately in _fill_local_items + if ( + self.yhorm_location.name == "Vordt of the Boreal Valley" or ( + self.yhorm_location.name == "Dancer of the Boreal Valley" and + not self.options.late_basin_of_vows + ) + ): + self.multiworld.local_early_items[self.player]["Storm Ruler"] = 1 + + def _allow_boss_for_yhorm(self, boss: DS3BossInfo) -> bool: + """Returns whether boss is a valid location for Yhorm in this seed.""" + + if not self.options.enable_dlc and boss.dlc: return False + + if not self._is_location_available("PC: Storm Ruler - boss room"): + # If the Storm Ruler isn't randomized, make sure the player can get to the normal Storm + # Ruler location before they need to get through Yhorm. + if boss.before_storm_ruler: return False + + # If the Small Doll also wasn't randomized, make sure Yhorm isn't blocking access to it + # or it won't be possible to get into Profaned Capital before beating him. + if ( + not self._is_location_available("CD: Small Doll - boss drop") + and boss.name in {"Crystal Sage", "Deacons of the Deep"} + ): + return False + + if boss.name != "Iudex Gundyr": return True + + # Cemetery of Ash has very few locations and all of them are excluded by default, so only + # allow Yhorm as Iudex Gundyr if there's at least one available location. + return any( + self._is_location_available(location) + and location.name not in self.all_excluded_locations + and location.name != "CA: Coiled Sword - boss drop" + for location in location_tables["Cemetery of Ash"] + ) + + def create_regions(self) -> None: # Create Vanilla Regions - regions: Dict[str, Region] = {} - regions["Menu"] = self.create_region("Menu", progressive_location_table) + regions: Dict[str, Region] = {"Menu": self.create_region("Menu", {})} regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [ + "Cemetery of Ash", "Firelink Shrine", "Firelink Shrine Bell Tower", "High Wall of Lothric", @@ -138,18 +168,15 @@ def create_regions(self): "Untended Graves", "Archdragon Peak", "Kiln of the First Flame", + "Greirat's Shop", + "Karla's Shop", ]}) - # Adds Path of the Dragon as an event item for Archdragon Peak access - potd_location = DarkSouls3Location(self.player, "CKG: Path of the Dragon", DS3LocationCategory.EVENT, "Path of the Dragon", None, regions["Consumed King's Garden"]) - potd_location.place_locked_item(Item("Path of the Dragon", ItemClassification.progression, None, self.player)) - regions["Consumed King's Garden"].locations.append(potd_location) - # Create DLC Regions - if self.multiworld.enable_dlc[self.player]: + if self.options.enable_dlc: regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [ - "Painted World of Ariandel 1", - "Painted World of Ariandel 2", + "Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)", "Dreg Heap", "Ringed City", ]}) @@ -161,7 +188,9 @@ def create_connection(from_region: str, to_region: str): connection.connect(regions[to_region]) regions["Menu"].exits.append(Entrance(self.player, "New Game", regions["Menu"])) - self.multiworld.get_entrance("New Game", self.player).connect(regions["Firelink Shrine"]) + self.multiworld.get_entrance("New Game", self.player).connect(regions["Cemetery of Ash"]) + + create_connection("Cemetery of Ash", "Firelink Shrine") create_connection("Firelink Shrine", "High Wall of Lothric") create_connection("Firelink Shrine", "Firelink Shrine Bell Tower") @@ -169,6 +198,7 @@ def create_connection(from_region: str, to_region: str): create_connection("High Wall of Lothric", "Undead Settlement") create_connection("High Wall of Lothric", "Lothric Castle") + create_connection("High Wall of Lothric", "Greirat's Shop") create_connection("Undead Settlement", "Road of Sacrifices") @@ -185,6 +215,7 @@ def create_connection(from_region: str, to_region: str): create_connection("Irithyll Dungeon", "Archdragon Peak") create_connection("Irithyll Dungeon", "Profaned Capital") + create_connection("Irithyll Dungeon", "Karla's Shop") create_connection("Lothric Castle", "Consumed King's Garden") create_connection("Lothric Castle", "Grand Archives") @@ -192,357 +223,1349 @@ def create_connection(from_region: str, to_region: str): create_connection("Consumed King's Garden", "Untended Graves") # Connect DLC Regions - if self.multiworld.enable_dlc[self.player]: - create_connection("Cathedral of the Deep", "Painted World of Ariandel 1") - create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2") - create_connection("Painted World of Ariandel 2", "Dreg Heap") + if self.options.enable_dlc: + create_connection("Cathedral of the Deep", "Painted World of Ariandel (Before Contraption)") + create_connection("Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)") + create_connection("Painted World of Ariandel (After Contraption)", "Dreg Heap") create_connection("Dreg Heap", "Ringed City") - # For each region, add the associated locations retrieved from the corresponding location_table def create_region(self, region_name, location_table) -> Region: new_region = Region(region_name, self.player, self.multiworld) + # Use this to un-exclude event locations so the fill doesn't complain about items behind + # them being unreachable. + excluded = self.options.exclude_locations.value + for location in location_table: - if location.category in self.enabled_location_categories: - new_location = DarkSouls3Location( - self.player, - location.name, - location.category, - location.default_item, - self.location_name_to_id[location.name], - new_region - ) + if self._is_location_available(location): + new_location = DarkSouls3Location(self.player, location, new_region) + if ( + # Exclude missable locations that don't allow useful items + location.missable and self.options.missable_location_behavior == "forbid_useful" + and not ( + # Unless they are excluded to a higher degree already + location.name in self.all_excluded_locations + and self.options.missable_location_behavior < self.options.excluded_location_behavior + ) + ) or ( + # Lift Chamber Key is missable. Exclude Lift-Chamber-Key-Locked locations if it isn't randomized + not self._is_location_available("FS: Lift Chamber Key - Leonhard") + and location.name == "HWL: Red Eye Orb - wall tower, miniboss" + ) or ( + # Chameleon is missable. Exclude Chameleon-locked locations if it isn't randomized + not self._is_location_available("AL: Chameleon - tomb after marrying Anri") + and location.name in {"RC: Dragonhead Shield - streets monument, across bridge", + "RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "RC: Divine Blessing - streets monument, mob drop", "RC: Lapp's Helm - Lapp", + "RC: Lapp's Armor - Lapp", + "RC: Lapp's Gauntlets - Lapp", + "RC: Lapp's Leggings - Lapp"} + ): + new_location.progress_type = LocationProgressType.EXCLUDED else: - # Replace non-randomized progression items with events - event_item = self.create_item(location.default_item) - if event_item.classification != ItemClassification.progression: + # Don't allow missable duplicates of progression items to be expected progression. + if location.name in {"PC: Storm Ruler - Siegward", + "US: Pyromancy Flame - Cornyx", + "US: Tower Key - kill Irina"}: continue + # Replace non-randomized items with events that give the default item + event_item = ( + self.create_item(location.default_item_name) if location.default_item_name + else DarkSouls3Item.event(location.name, self.player) + ) + new_location = DarkSouls3Location( self.player, - location.name, - location.category, - location.default_item, - None, - new_region + location, + parent = new_region, + event = True, ) event_item.code = None new_location.place_locked_item(event_item) - - if region_name == "Menu": - add_item_rule(new_location, lambda item: not item.advancement) + if location.name in excluded: + excluded.remove(location.name) + # Only remove from all_excluded if excluded does not have priority over missable + if not (self.options.missable_location_behavior < self.options.excluded_location_behavior): + self.all_excluded_locations.remove(location.name) new_region.locations.append(new_location) self.multiworld.regions.append(new_region) return new_region - - def create_items(self): - dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true - - itempool_by_category = {category: [] for category in self.enabled_location_categories} + def create_items(self) -> None: + # Just used to efficiently deduplicate items + item_set: Set[str] = set() # Gather all default items on randomized locations + self.local_itempool = [] num_required_extra_items = 0 - for location in self.multiworld.get_locations(self.player): - if location.category in itempool_by_category: - if item_dictionary[location.default_item_name].category == DS3ItemCategory.SKIP: + for location in cast(List[DarkSouls3Location], self.multiworld.get_unfilled_locations(self.player)): + if not self._is_location_available(location.name): + raise Exception("DS3 generation bug: Added an unavailable location.") + + default_item_name = cast(str, location.data.default_item_name) + item = item_dictionary[default_item_name] + if item.skip: + num_required_extra_items += 1 + elif not item.unique: + self.local_itempool.append(self.create_item(default_item_name)) + else: + # For unique items, make sure there aren't duplicates in the item set even if there + # are multiple in-game locations that provide them. + if default_item_name in item_set: num_required_extra_items += 1 else: - itempool_by_category[location.category].append(location.default_item_name) - - # Replace each item category with a random sample of items of those types - if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various: - def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int): - candidates = [ - item.name for item - in item_dictionary.values() - if (item.category in item_categories and (not item.is_dlc or dlc_enabled)) - ] - return self.multiworld.random.sample(candidates, num_items) - - if DS3LocationCategory.WEAPON in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.WEAPON] = create_random_replacement_list( - { - DS3ItemCategory.WEAPON_UPGRADE_5, - DS3ItemCategory.WEAPON_UPGRADE_10, - DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE - }, - len(itempool_by_category[DS3LocationCategory.WEAPON]) - ) - if DS3LocationCategory.SHIELD in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.SHIELD] = create_random_replacement_list( - {DS3ItemCategory.SHIELD, DS3ItemCategory.SHIELD_INFUSIBLE}, - len(itempool_by_category[DS3LocationCategory.SHIELD]) - ) - if DS3LocationCategory.ARMOR in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.ARMOR] = create_random_replacement_list( - {DS3ItemCategory.ARMOR}, - len(itempool_by_category[DS3LocationCategory.ARMOR]) - ) - if DS3LocationCategory.RING in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.RING] = create_random_replacement_list( - {DS3ItemCategory.RING}, - len(itempool_by_category[DS3LocationCategory.RING]) - ) - if DS3LocationCategory.SPELL in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.SPELL] = create_random_replacement_list( - {DS3ItemCategory.SPELL}, - len(itempool_by_category[DS3LocationCategory.SPELL]) - ) - - itempool: List[DarkSouls3Item] = [] - for category in self.enabled_location_categories: - itempool += [self.create_item(name) for name in itempool_by_category[category]] - - # A list of items we can replace - removable_items = [item for item in itempool if item.classification != ItemClassification.progression] - - guaranteed_items = self.multiworld.guaranteed_items[self.player].value - for item_name in guaranteed_items: - # Break early just in case nothing is removable (if user is trying to guarantee more - # items than the pool can hold, for example) - if len(removable_items) == 0: - break - - num_existing_copies = len([item for item in itempool if item.name == item_name]) - for _ in range(guaranteed_items[item_name]): - if num_existing_copies > 0: - num_existing_copies -= 1 - continue - - if num_required_extra_items > 0: - # We can just add them instead of using "Soul of an Intrepid Hero" later - num_required_extra_items -= 1 - else: - if len(removable_items) == 0: - break + item_set.add(default_item_name) + self.local_itempool.append(self.create_item(default_item_name)) - # Try to construct a list of items with the same category that can be removed - # If none exist, just remove something at random - removable_shortlist = [ - item for item - in removable_items - if item_dictionary[item.name].category == item_dictionary[item_name].category - ] - if len(removable_shortlist) == 0: - removable_shortlist = removable_items + injectables = self._create_injectable_items(num_required_extra_items) + num_required_extra_items -= len(injectables) + self.local_itempool.extend(injectables) - removed_item = self.multiworld.random.choice(removable_shortlist) - removable_items.remove(removed_item) # To avoid trying to replace the same item twice - itempool.remove(removed_item) + # Extra filler items for locations containing skip items + self.local_itempool.extend(self.create_item(self.get_filler_item_name()) for _ in range(num_required_extra_items)) - itempool.append(self.create_item(item_name)) - - # Extra filler items for locations containing SKIP items - itempool += [self.create_filler() for _ in range(num_required_extra_items)] + # Potentially fill some items locally and remove them from the itempool + self._fill_local_items() # Add items to itempool - self.multiworld.itempool += itempool - - - def create_item(self, name: str) -> Item: - useful_categories = { - DS3ItemCategory.WEAPON_UPGRADE_5, - DS3ItemCategory.WEAPON_UPGRADE_10, - DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, - DS3ItemCategory.SPELL, - } - data = self.item_name_to_id[name] - - if name in key_item_names: - item_classification = ItemClassification.progression - elif item_dictionary[name].category in useful_categories or name in {"Estus Shard", "Undead Bone Shard"}: - item_classification = ItemClassification.useful - else: - item_classification = ItemClassification.filler - - return DarkSouls3Item(name, item_classification, data, self.player) + self.multiworld.itempool += self.local_itempool + + def _create_injectable_items(self, num_required_extra_items: int) -> List[DarkSouls3Item]: + """Returns a list of items to inject into the multiworld instead of skipped items. + + If there isn't enough room to inject all the necessary progression items + that are in missable locations by default, this adds them to the + player's starting inventory. + """ + + all_injectable_items = [ + item for item + in item_dictionary.values() + if item.inject and (not item.is_dlc or self.options.enable_dlc) + ] + injectable_mandatory = [ + item for item in all_injectable_items + if item.classification == ItemClassification.progression + ] + injectable_optional = [ + item for item in all_injectable_items + if item.classification != ItemClassification.progression + ] + + number_to_inject = min(num_required_extra_items, len(all_injectable_items)) + items = ( + self.random.sample( + injectable_mandatory, + k=min(len(injectable_mandatory), number_to_inject) + ) + + self.random.sample( + injectable_optional, + k=max(0, number_to_inject - len(injectable_mandatory)) + ) + ) + + if number_to_inject < len(injectable_mandatory): + # It's worth considering the possibility of _removing_ unimportant + # items from the pool to inject these instead rather than just + # making them part of the starting health back + for item in injectable_mandatory: + if item in items: continue + self.multiworld.push_precollected(self.create_item(item)) + warning( + f"Couldn't add \"{item.name}\" to the item pool for " + + f"{self.player_name}. Adding it to the starting " + + f"inventory instead." + ) + return [self.create_item(item) for item in items] + + def create_item(self, item: Union[str, DS3ItemData]) -> DarkSouls3Item: + data = item if isinstance(item, DS3ItemData) else item_dictionary[item] + classification = None + if self.multiworld and data.useful_if != UsefulIf.DEFAULT and ( + ( + data.useful_if == UsefulIf.BASE and + not self.options.enable_dlc and + not self.options.enable_ngp + ) + or (data.useful_if == UsefulIf.NO_DLC and not self.options.enable_dlc) + or (data.useful_if == UsefulIf.NO_NGP and not self.options.enable_ngp) + ): + classification = ItemClassification.useful + + if ( + self.options.randomize_weapon_level != "none" + and data.category.upgrade_level + # Because we require the Pyromancy Flame to be available early, don't upgrade it so it + # doesn't get shuffled around by weapon smoothing. + and data.name != "Pyromancy Flame" + ): + # if the user made an error and set a min higher than the max we default to the max + max_5 = self.options.max_levels_in_5.value + min_5 = min(self.options.min_levels_in_5.value, max_5) + max_10 = self.options.max_levels_in_10.value + min_10 = min(self.options.min_levels_in_10.value, max_10) + weapon_level_percentage = self.options.randomize_weapon_level_percentage + + if self.random.randint(0, 99) < weapon_level_percentage: + if data.category.upgrade_level == 5: + data = data.upgrade(self.random.randint(min_5, max_5)) + elif data.category.upgrade_level == 10: + data = data.upgrade(self.random.randint(min_10, max_10)) + + if self.options.randomize_infusion and data.category.is_infusible: + infusion_percentage = self.options.randomize_infusion_percentage + if self.random.randint(0, 99) < infusion_percentage: + data = data.infuse(self.random.choice(list(Infusion))) + + return DarkSouls3Item(self.player, data, classification=classification) + + def _fill_local_items(self) -> None: + """Removes certain items from the item pool and manually places them in the local world. + + We can't do this in pre_fill because the itempool may not be modified after create_items. + """ + # If Yhorm is at Iudex Gundyr, Storm Ruler must be randomized, so it can always be moved. + # Fill this manually so that, if very few slots are available in Cemetery of Ash, this + # doesn't get locked out by bad rolls on the next two fills. + if self.yhorm_location.name == "Iudex Gundyr": + self._fill_local_item("Storm Ruler", ["Cemetery of Ash"], + lambda location: location.name != "CA: Coiled Sword - boss drop") + + # If the Coiled Sword is vanilla, it is early enough and doesn't need to be placed. + # Don't place this in the multiworld because it's necessary almost immediately, and don't + # mark it as a blocker for HWL because having a miniscule Sphere 1 screws with progression balancing. + if self._is_location_available("CA: Coiled Sword - boss drop"): + self._fill_local_item("Coiled Sword", ["Cemetery of Ash", "Firelink Shrine"]) + + # If the HWL Raw Gem is vanilla, it is early enough and doesn't need to be removed. If + # upgrade smoothing is enabled, make sure one raw gem is available early for SL1 players + if ( + self._is_location_available("HWL: Raw Gem - fort roof, lizard") + and self.options.smooth_upgrade_items + ): + self._fill_local_item("Raw Gem", [ + "Cemetery of Ash", + "Firelink Shrine", + "High Wall of Lothric" + ]) + + def _fill_local_item( + self, name: str, + regions: List[str], + additional_condition: Optional[Callable[[DS3LocationData], bool]] = None, + ) -> None: + """Chooses a valid location for the item with the given name and places it there. + + This always chooses a local location among the given regions. If additional_condition is + passed, only locations meeting that condition will be considered. + + If the item could not be placed, it will be added to starting inventory. + """ + item = next((item for item in self.local_itempool if item.name == name), None) + if not item: return + + candidate_locations = [ + location for location in ( + self.multiworld.get_location(location.name, self.player) + for region in regions + for location in location_tables[region] + if self._is_location_available(location) + and not location.missable + and not location.conditional + and (not additional_condition or additional_condition(location)) + ) + # We can't use location.progress_type here because it's not set + # until after `set_rules()` runs. + if not location.item and location.name not in self.all_excluded_locations + and location.item_rule(item) + ] + + self.local_itempool.remove(item) + + if not candidate_locations: + warning(f"Couldn't place \"{name}\" in a valid location for {self.player_name}. Adding it to starting inventory instead.") + location = next( + (location for location in self._get_our_locations() if location.data.default_item_name == item.name), + None + ) + if location: self._replace_with_filler(location) + self.multiworld.push_precollected(self.create_item(name)) + return + + location = self.random.choice(candidate_locations) + location.place_locked_item(item) + + def _replace_with_filler(self, location: DarkSouls3Location) -> None: + """If possible, choose a filler item to replace location's current contents with.""" + if location.locked: return + + # Try 10 filler items. If none of them work, give up and leave it as-is. + for _ in range(0, 10): + candidate = self.create_filler() + if location.item_rule(candidate): + location.item = candidate + return def get_filler_item_name(self) -> str: - return "Soul of an Intrepid Hero" - + return self.random.choice(filler_item_names) def set_rules(self) -> None: - # Define the access rules to the entrances - set_rule(self.multiworld.get_entrance("Go To Undead Settlement", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) - set_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player), - lambda state: state.has("Basin of Vows", self.player)) - set_rule(self.multiworld.get_entrance("Go To Irithyll of the Boreal Valley", self.player), - lambda state: state.has("Small Doll", self.player)) - set_rule(self.multiworld.get_entrance("Go To Archdragon Peak", self.player), - lambda state: state.has("Path of the Dragon", self.player)) - set_rule(self.multiworld.get_entrance("Go To Grand Archives", self.player), - lambda state: state.has("Grand Archives Key", self.player)) - set_rule(self.multiworld.get_entrance("Go To Kiln of the First Flame", self.player), - lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and - state.has("Cinders of a Lord - Yhorm the Giant", self.player) and - state.has("Cinders of a Lord - Aldrich", self.player) and - state.has("Cinders of a Lord - Lothric Prince", self.player)) - - if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) + randomized_items = {item.name for item in self.local_itempool} + + self._add_shop_rules() + self._add_npc_rules() + self._add_transposition_rules() + self._add_crow_rules() + self._add_allow_useful_location_rules() + self._add_early_item_rules(randomized_items) + + self._add_entrance_rule("Firelink Shrine Bell Tower", "Tower Key") + self._add_entrance_rule("Undead Settlement", lambda state: ( + state.has("Small Lothric Banner", self.player) + and self._can_get(state, "HWL: Soul of Boreal Valley Vordt") + )) + self._add_entrance_rule("Road of Sacrifices", "US -> RS") + self._add_entrance_rule( + "Cathedral of the Deep", + lambda state: self._can_get(state, "RS: Soul of a Crystal Sage") + ) + self._add_entrance_rule("Farron Keep", "RS -> FK") + self._add_entrance_rule( + "Catacombs of Carthus", + lambda state: self._can_get(state, "FK: Soul of the Blood of the Wolf") + ) + self._add_entrance_rule("Irithyll Dungeon", "IBV -> ID") + self._add_entrance_rule( + "Lothric Castle", + lambda state: self._can_get(state, "HWL: Soul of the Dancer") + ) + self._add_entrance_rule( + "Untended Graves", + lambda state: self._can_get(state, "CKG: Soul of Consumed Oceiros") + ) + self._add_entrance_rule("Irithyll of the Boreal Valley", lambda state: ( + state.has("Small Doll", self.player) + and self._can_get(state, "CC: Soul of High Lord Wolnir") + )) + self._add_entrance_rule( + "Anor Londo", + lambda state: self._can_get(state, "IBV: Soul of Pontiff Sulyvahn") + ) + self._add_entrance_rule("Archdragon Peak", "Path of the Dragon") + self._add_entrance_rule("Grand Archives", lambda state: ( + state.has("Grand Archives Key", self.player) + and self._can_get(state, "LC: Soul of Dragonslayer Armour") + )) + self._add_entrance_rule("Kiln of the First Flame", lambda state: ( + state.has("Cinders of a Lord - Abyss Watcher", self.player) + and state.has("Cinders of a Lord - Yhorm the Giant", self.player) + and state.has("Cinders of a Lord - Aldrich", self.player) + and state.has("Cinders of a Lord - Lothric Prince", self.player) + and state.has("Transposing Kiln", self.player) + )) + + if self.options.late_basin_of_vows: + self._add_entrance_rule("Lothric Castle", lambda state: ( + state.has("Small Lothric Banner", self.player) + # Make sure these are actually available early. + and ( + "Transposing Kiln" not in randomized_items + or state.has("Transposing Kiln", self.player) + ) and ( + "Pyromancy Flame" not in randomized_items + or state.has("Pyromancy Flame", self.player) + ) + # This isn't really necessary, but it ensures that the game logic knows players will + # want to do Lothric Castle after at least being _able_ to access Catacombs. This is + # useful for smooth item placement. + and self._has_any_scroll(state) + )) + + if self.options.late_basin_of_vows > 1: # After Small Doll + self._add_entrance_rule("Lothric Castle", "Small Doll") # DLC Access Rules Below - if self.multiworld.enable_dlc[self.player]: - set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player), - lambda state: state.has("Small Envoy Banner", self.player)) + if self.options.enable_dlc: + self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "CD -> PW1") + self._add_entrance_rule("Painted World of Ariandel (After Contraption)", "Contraption Key") + self._add_entrance_rule( + "Dreg Heap", + lambda state: self._can_get(state, "PW2: Soul of Sister Friede") + ) + self._add_entrance_rule("Ringed City", lambda state: ( + state.has("Small Envoy Banner", self.player) + and self._can_get(state, "DH: Soul of the Demon Prince") + )) - # If key items are randomized, must have contraption key to enter second half of Ashes DLC - # If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed - if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player), - lambda state: state.has("Contraption Key", self.player)) + if self.options.late_dlc: + self._add_entrance_rule( + "Painted World of Ariandel (Before Contraption)", + lambda state: state.has("Small Doll", self.player) and self._has_any_scroll(state)) - if self.multiworld.late_dlc[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player), - lambda state: state.has("Small Doll", self.player)) + if self.options.late_dlc > 1: # After Basin + self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") # Define the access rules to some specific locations - set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player), - lambda state: state.has("Storm Ruler", self.player)) - - if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player), - lambda state: state.has("Jailbreaker's Key", self.player)) - set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player), - lambda state: state.has("Old Cell Key", self.player)) - set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) - - if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player), - lambda state: state.has("Cell Key", self.player)) - set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player), - lambda state: state.has("Cell Key", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Pointed Hat", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Coat", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Gloves", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - - if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - - if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player), - lambda state: state.has("Storm Ruler", self.player)) - set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), - lambda state: state.has("Basin of Vows", self.player)) - - # Lump Soul of the Dancer in with LC for locations that should not be reachable - # before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US) - if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) - - gotthard_corpse_rule = lambda state: \ - (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and - state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) - - set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule) - - if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule) - - self.multiworld.completion_condition[self.player] = lambda state: \ - state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ - state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ - state.has("Cinders of a Lord - Aldrich", self.player) and \ - state.has("Cinders of a Lord - Lothric Prince", self.player) + if self._is_location_available("FS: Lift Chamber Key - Leonhard"): + self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", + "Lift Chamber Key") + self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", + "Jailbreaker's Key") + self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") + self._add_location_rule([ + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss" + ], lambda state: self._can_get(state, "FK: Cinders of a Lord - Abyss Watcher")) + self._add_location_rule( + "ID: Prisoner Chief's Ashes - B2 near, locked cell by stairs", + "Jailer's Key Ring" + ) + self._add_entrance_rule("Karla's Shop", "Jailer's Key Ring") + + # The static randomizer edits events to guarantee that Greirat won't go to Lothric until + # Grand Archives is available, so his shop will always be available one way or another. + self._add_entrance_rule("Greirat's Shop", "Cell Key") + + self._add_location_rule("HWL: Soul of the Dancer", "Basin of Vows") + + # Lump Soul of the Dancer in with LC for locations that should not be reachable + # before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US) + if self.options.late_basin_of_vows: + self._add_location_rule("HWL: Soul of the Dancer", lambda state: ( + state.has("Small Lothric Banner", self.player) + # Make sure these are actually available early. + and ( + "Transposing Kiln" not in randomized_items + or state.has("Transposing Kiln", self.player) + ) and ( + "Pyromancy Flame" not in randomized_items + or state.has("Pyromancy Flame", self.player) + ) + # This isn't really necessary, but it ensures that the game logic knows players will + # want to do Lothric Castle after at least being _able_ to access Catacombs. This is + # useful for smooth item placement. + and self._has_any_scroll(state) + )) + + if self.options.late_basin_of_vows > 1: # After Small Doll + self._add_location_rule("HWL: Soul of the Dancer", "Small Doll") + + self._add_location_rule([ + "LC: Grand Archives Key - by Grand Archives door, after PC and AL bosses", + "LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bosses" + ], lambda state: ( + self._can_get(state, "AL: Cinders of a Lord - Aldrich") and + self._can_get(state, "PC: Cinders of a Lord - Yhorm the Giant") + )) + + self._add_location_rule([ + "FS: Morne's Great Hammer - Eygon", + "FS: Moaning Shield - Eygon" + ], lambda state: ( + self._can_get(state, "LC: Soul of Dragonslayer Armour") and + self._can_get(state, "FK: Soul of the Blood of the Wolf") + )) + + self._add_location_rule([ + "CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + ], lambda state: self._can_go_to(state, "Archdragon Peak")) + + self._add_location_rule([ + "FK: Havel's Helm - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Armor - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Leggings - upper keep, after killing AP belfry roof NPC", + ], lambda state: self._can_go_to(state, "Archdragon Peak")) + + self._add_location_rule([ + "RC: Dragonhead Shield - streets monument, across bridge", + "RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "RC: Divine Blessing - streets monument, mob drop", + "RC: Lapp's Helm - Lapp", + "RC: Lapp's Armor - Lapp", + "RC: Lapp's Gauntlets - Lapp", + "RC: Lapp's Leggings - Lapp", + ], "Chameleon") + + # Forbid shops from carrying items with multiple counts (the static randomizer has its own + # logic for choosing how many shop items to sell), and from carrying soul items. + for location in location_dictionary.values(): + if location.shop: + self._add_item_rule( + location.name, + lambda item: ( + item.player != self.player or + (item.data.count == 1 and not item.data.souls) + ) + ) + + # This particular location is bugged, and will drop two copies of whatever item is placed + # there. + if self._is_location_available("US: Young White Branch - by white tree #2"): + self._add_item_rule( + "US: Young White Branch - by white tree #2", + lambda item: item.player == self.player and not item.data.unique + ) + + # Make sure the Storm Ruler is available BEFORE Yhorm the Giant + if self.yhorm_location.name == "Ancient Wyvern": + # This is a white lie, you can get to a bunch of items in AP before you beat the Wyvern, + # but this saves us from having to split the entire region in two just to mark which + # specific items are before and after. + self._add_entrance_rule("Archdragon Peak", "Storm Ruler") + for location in self.yhorm_location.locations: + self._add_location_rule(location, "Storm Ruler") + + self.multiworld.completion_condition[self.player] = lambda state: self._can_get(state, "KFF: Soul of the Lords") + + def _add_shop_rules(self) -> None: + """Adds rules for items unlocked in shops.""" + + # Ashes + ashes = { + "Mortician's Ashes": ["Alluring Skull", "Ember", "Grave Key"], + "Dreamchaser's Ashes": ["Life Ring", "Hidden Blessing"], + "Paladin's Ashes": ["Lloyd's Shield Ring"], + "Grave Warden's Ashes": ["Ember"], + "Prisoner Chief's Ashes": [ + "Karla's Pointed Hat", "Karla's Coat", "Karla's Gloves", "Karla's Trousers" + ], + "Xanthous Ashes": ["Xanthous Overcoat", "Xanthous Gloves", "Xanthous Trousers"], + "Dragon Chaser's Ashes": ["Ember"], + "Easterner's Ashes": [ + "Washing Pole", "Eastern Helm", "Eastern Armor", "Eastern Gauntlets", + "Eastern Leggings", "Wood Grain Ring", + ], + "Captain's Ashes": [ + "Millwood Knight Helm", "Millwood Knight Armor", "Millwood Knight Gauntlets", + "Millwood Knight Leggings", "Refined Gem", + ] + } + for (ash, items) in ashes.items(): + self._add_location_rule([f"FS: {item} - {ash}" for item in items], ash) + + # Shop unlocks + shop_unlocks = { + "Cornyx": [ + ( + "Great Swamp Pyromancy Tome", "Great Swamp Tome", + ["Poison Mist", "Fire Orb", "Profuse Sweat", "Bursting Fireball"] + ), + ( + "Carthus Pyromancy Tome", "Carthus Tome", + ["Acid Surge", "Carthus Flame Arc", "Carthus Beacon"] + ), + ("Izalith Pyromancy Tome", "Izalith Tome", ["Great Chaos Fire Orb", "Chaos Storm"]), + ], + "Irina": [ + ( + "Braille Divine Tome of Carim", "Tome of Carim", + ["Med Heal", "Tears of Denial", "Force"] + ), + ( + "Braille Divine Tome of Lothric", "Tome of Lothric", + ["Bountiful Light", "Magic Barrier", "Blessed Weapon"] + ), + ], + "Orbeck": [ + ("Sage's Scroll", "Sage's Scroll", ["Great Farron Dart", "Farron Hail"]), + ( + "Golden Scroll", "Golden Scroll", + [ + "Cast Light", "Repair", "Hidden Weapon", "Hidden Body", + "Twisted Wall of Light" + ], + ), + ("Logan's Scroll", "Logan's Scroll", ["Homing Soulmass", "Soul Spear"]), + ( + "Crystal Scroll", "Crystal Scroll", + ["Homing Crystal Soulmass", "Crystal Soul Spear", "Crystal Magic Weapon"] + ), + ], + "Karla": [ + ("Quelana Pyromancy Tome", "Quelana Tome", ["Firestorm", "Rapport", "Fire Whip"]), + ( + "Grave Warden Pyromancy Tome", "Grave Warden Tome", + ["Black Flame", "Black Fire Orb"] + ), + ("Deep Braille Divine Tome", "Deep Braille Tome", ["Gnaw", "Deep Protection"]), + ( + "Londor Braille Divine Tome", "Londor Tome", + ["Vow of Silence", "Dark Blade", "Dead Again"] + ), + ], + } + for (shop, unlocks) in shop_unlocks.items(): + for (key, key_name, items) in unlocks: + self._add_location_rule( + [f"FS: {item} - {shop} for {key_name}" for item in items], key) + + def _add_npc_rules(self) -> None: + """Adds rules for items accessible via NPC quests. + + We list missable locations here even though they never contain progression items so that the + game knows what sphere they're in. This is especially useful for item smoothing. (We could + add rules for boss transposition items as well, but then we couldn't freely reorder boss + soul locations for smoothing.) + + Generally, for locations that can be accessed early by killing NPCs, we set up requirements + assuming the player _doesn't_ so they aren't forced to start killing allies to advance the + quest. + """ + + ## Greirat + + self._add_location_rule([ + "FS: Divine Blessing - Greirat from US", + "FS: Ember - Greirat from US", + ], lambda state: ( + self._can_go_to(state, "Undead Settlement") + and state.has("Loretta's Bone", self.player) + )) + self._add_location_rule([ + "FS: Divine Blessing - Greirat from IBV", + "FS: Hidden Blessing - Greirat from IBV", + "FS: Titanite Scale - Greirat from IBV", + "FS: Twinkling Titanite - Greirat from IBV", + "FS: Ember - shop for Greirat's Ashes" + ], lambda state: ( + self._can_go_to(state, "Irithyll of the Boreal Valley") + and self._can_get(state, "FS: Divine Blessing - Greirat from US") + # Either Patches or Siegward can save Greirat, but we assume the player will want to use + # Patches because it's harder to screw up + and self._can_get(state, "CD: Shotel - Patches") + )) + self._add_location_rule([ + "FS: Ember - shop for Greirat's Ashes", + ], lambda state: ( + self._can_go_to(state, "Grand Archives") + and self._can_get(state, "FS: Divine Blessing - Greirat from IBV") + )) + + ## Patches + + # Patches will only set up shop in Firelink once he's tricked you in the bell tower. He'll + # only do _that_ once you've spoken to Siegward after killing the Fire Demon and lit the + # Rosaria's Bed Chamber bonfire. He _won't_ set up shop in the Cathedral if you light the + # Rosaria's Bed Chamber bonfire before getting tricked by him, so we assume these locations + # require the bell tower. + self._add_location_rule([ + "CD: Shotel - Patches", + "CD: Ember - Patches", + "FS: Rusted Gold Coin - don't forgive Patches" + ], lambda state: ( + self._can_go_to(state, "Firelink Shrine Bell Tower") + and self._can_go_to(state, "Cathedral of the Deep") + )) + + # Patches sells this after you tell him to search for Greirat in Grand Archives + self._add_location_rule([ + "FS: Hidden Blessing - Patches after searching GA" + ], lambda state: ( + self._can_get(state, "CD: Shotel - Patches") + and self._can_get(state, "FS: Ember - shop for Greirat's Ashes") + )) + + # Only make the player kill Patches once all his other items are available + self._add_location_rule([ + "CD: Winged Spear - kill Patches", + # You don't _have_ to kill him for this, but he has to be in Firelink at the same time + # as Greirat to get it in the shop and that may not be feasible if the player progresses + # Greirat's quest much faster. + "CD: Horsehoof Ring - Patches", + ], lambda state: ( + self._can_get(state, "FS: Hidden Blessing - Patches after searching GA") + and self._can_get(state, "FS: Rusted Gold Coin - don't forgive Patches") + )) + + ## Leonhard + + self._add_location_rule([ + # Talk to Leonhard in Firelink with a Pale Tongue after lighting Cliff Underside or + # killing Greatwood. This doesn't consume the Pale Tongue, it just has to be in + # inventory + "FS: Lift Chamber Key - Leonhard", + # Progress Leonhard's quest and then return to Rosaria after lighting Profaned Capital + "CD: Black Eye Orb - Rosaria from Leonhard's quest", + ], "Pale Tongue") + + self._add_location_rule([ + "CD: Black Eye Orb - Rosaria from Leonhard's quest", + ], lambda state: ( + # The Black Eye Orb location won't spawn until you kill the HWL miniboss and resting at + # the Profaned Capital bonfire. + self._can_get(state, "HWL: Red Eye Orb - wall tower, miniboss") + and self._can_go_to(state, "Profaned Capital") + )) + + # Perhaps counterintuitively, you CAN fight Leonhard before you access the location that + # would normally give you the Black Eye Orb. + self._add_location_rule([ + "AL: Crescent Moon Sword - Leonhard drop", + "AL: Silver Mask - Leonhard drop", + "AL: Soul of Rosaria - Leonhard drop", + ] + [ + f"FS: {item} - shop after killing Leonhard" + for item in ["Leonhard's Garb", "Leonhard's Gauntlets", "Leonhard's Trousers"] + ], "Black Eye Orb") + + ## Hawkwood + + # After Hawkwood leaves and once you have the Torso Stone, you can fight him for dragon + # stones. Andre will give Swordgrass as a hint as well + self._add_location_rule([ + "FK: Twinkling Dragon Head Stone - Hawkwood drop", + "FS: Hawkwood's Swordgrass - Andre after gesture in AP summit" + ], lambda state: ( + self._can_get(state, "FS: Hawkwood's Shield - gravestone after Hawkwood leaves") + and state.has("Twinkling Dragon Torso Stone", self.player) + )) + + ## Siegward + + # Unlock Siegward's cell after progressing his quest + self._add_location_rule([ + "ID: Titanite Slab - Siegward", + ], lambda state: ( + state.has("Old Cell Key", self.player) + # Progressing Siegward's quest requires buying his armor from Patches. + and self._can_get(state, "CD: Shotel - Patches") + )) + + # These drop after completing Siegward's quest and talking to him in Yhorm's arena + self._add_location_rule([ + "PC: Siegbräu - Siegward after killing boss", + "PC: Storm Ruler - Siegward", + "PC: Pierce Shield - Siegward", + ], lambda state: ( + self._can_get(state, "ID: Titanite Slab - Siegward") + and self._can_get(state, "PC: Soul of Yhorm the Giant") + )) + + ## Sirris + + # Kill Greatwood and turn in Dreamchaser's Ashes to trigger this opportunity for invasion + self._add_location_rule([ + "FS: Mail Breaker - Sirris for killing Creighton", + "FS: Silvercat Ring - Sirris for killing Creighton", + "IBV: Creighton's Steel Mask - bridge after killing Creighton", + "IBV: Mirrah Chain Gloves - bridge after killing Creighton", + "IBV: Mirrah Chain Leggings - bridge after killing Creighton", + "IBV: Mirrah Chain Mail - bridge after killing Creighton", + "IBV: Dragonslayer's Axe - Creighton drop", + # Killing Pontiff without progressing Sirris's quest will break it. + "IBV: Soul of Pontiff Sulyvahn" + ], lambda state: ( + self._can_get(state, "US: Soul of the Rotted Greatwood") + and state.has("Dreamchaser's Ashes", self.player) + )) + # Add indirect condition since reaching AL requires defeating Pontiff which requires defeating Greatwood in US + self.multiworld.register_indirect_condition( + self.get_region("Undead Settlement"), + self.get_entrance("Go To Anor Londo") + ) + + # Kill Creighton and Aldrich to trigger this opportunity for invasion + self._add_location_rule([ + "FS: Budding Green Blossom - shop after killing Creighton and AL boss", + "FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + ], lambda state: ( + self._can_get(state, "FS: Mail Breaker - Sirris for killing Creighton") + and self._can_get(state, "AL: Soul of Aldrich") + )) + + # Kill Hodrick and Twin Princes to trigger the end of the quest + self._add_location_rule([ + "FS: Sunless Talisman - Sirris, kill GA boss", + "FS: Sunless Veil - shop, Sirris quest, kill GA boss", + "FS: Sunless Armor - shop, Sirris quest, kill GA boss", + "FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + # Killing Yorshka will anger Sirris and stop her quest, so don't expect it until the + # quest is done + "AL: Yorshka's Chime - kill Yorshka", + ], lambda state: ( + self._can_get(state, "US: Soul of the Rotted Greatwood") + and state.has("Dreamchaser's Ashes", self.player) + )) + + ## Cornyx + + self._add_location_rule([ + "US: Old Sage's Blindfold - kill Cornyx", + "US: Cornyx's Garb - kill Cornyx", + "US: Cornyx's Wrap - kill Cornyx", + "US: Cornyx's Skirt - kill Cornyx", + ], lambda state: ( + state.has("Great Swamp Pyromancy Tome", self.player) + and state.has("Carthus Pyromancy Tome", self.player) + and state.has("Izalith Pyromancy Tome", self.player) + )) + + self._add_location_rule([ + "US: Old Sage's Blindfold - kill Cornyx", "US: Cornyx's Garb - kill Cornyx", + "US: Cornyx's Wrap - kill Cornyx", "US: Cornyx's Skirt - kill Cornyx" + ], lambda state: ( + state.has("Great Swamp Pyromancy Tome", self.player) + and state.has("Carthus Pyromancy Tome", self.player) + and state.has("Izalith Pyromancy Tome", self.player) + )) + + ## Irina + + self._add_location_rule([ + "US: Tower Key - kill Irina", + ], lambda state: ( + state.has("Braille Divine Tome of Carim", self.player) + and state.has("Braille Divine Tome of Lothric", self.player) + )) + + ## Karla + + self._add_location_rule([ + "FS: Karla's Pointed Hat - kill Karla", + "FS: Karla's Coat - kill Karla", + "FS: Karla's Gloves - kill Karla", + "FS: Karla's Trousers - kill Karla", + ], lambda state: ( + state.has("Quelana Pyromancy Tome", self.player) + and state.has("Grave Warden Pyromancy Tome", self.player) + and state.has("Deep Braille Divine Tome", self.player) + and state.has("Londor Braille Divine Tome", self.player) + )) + + ## Emma + + self._add_location_rule("HWL: Basin of Vows - Emma", "Small Doll") + + ## Orbeck + + self._add_location_rule([ + "FS: Morion Blade - Yuria for Orbeck's Ashes", + "FS: Clandestine Coat - shop with Orbeck's Ashes" + ], lambda state: ( + state.has("Golden Scroll", self.player) + and state.has("Logan's Scroll", self.player) + and state.has("Crystal Scroll", self.player) + and state.has("Sage's Scroll", self.player) + )) + + self._add_location_rule([ + "FS: Pestilent Mist - Orbeck for any scroll", + "FS: Young Dragon Ring - Orbeck for one scroll and buying three spells", + # Make sure that the player can keep Orbeck around by giving him at least one scroll + # before killing Abyss Watchers. + "FK: Soul of the Blood of the Wolf", + "FK: Cinders of a Lord - Abyss Watcher", + "FS: Undead Legion Helm - shop after killing FK boss", + "FS: Undead Legion Armor - shop after killing FK boss", + "FS: Undead Legion Gauntlet - shop after killing FK boss", + "FS: Undead Legion Leggings - shop after killing FK boss", + "FS: Farron Ring - Hawkwood", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss", + ], self._has_any_scroll) + + # Not really necessary but ensures players can decide which way to go + if self.options.enable_dlc: + self._add_entrance_rule( + "Painted World of Ariandel (After Contraption)", + self._has_any_scroll + ) + + ## Anri + + # Anri only leaves Road of Sacrifices once Deacons is defeated + self._add_location_rule([ + "IBV: Ring of the Evil Eye - Anri", + "AL: Chameleon - tomb after marrying Anri", + ], lambda state: self._can_get(state, "CD: Soul of the Deacons of the Deep")) + + # If the player does Anri's non-marriage quest, they'll need to defeat the AL boss as well + # before it's complete. + self._add_location_rule([ + "AL: Anri's Straight Sword - Anri quest", + "FS: Elite Knight Helm - shop after Anri quest", + "FS: Elite Knight Armor - shop after Anri quest", + "FS: Elite Knight Gauntlets - shop after Anri quest", + "FS: Elite Knight Leggings - shop after Anri quest", + ], lambda state: ( + self._can_get(state, "IBV: Ring of the Evil Eye - Anri") and + self._can_get(state, "AL: Soul of Aldrich") + )) + + def _add_transposition_rules(self) -> None: + """Adds rules for items obtainable from Ludleth by soul transposition.""" + + transpositions = [ + ( + "Soul of Boreal Valley Vordt", "Vordt", + ["Vordt's Great Hammer", "Pontiff's Left Eye"] + ), + ("Soul of Rosaria", "Rosaria", ["Bountiful Sunlight"]), + ("Soul of Aldrich", "Aldrich", ["Darkmoon Longbow", "Lifehunt Scythe"]), + ( + "Soul of the Rotted Greatwood", "Greatwood", + ["Hollowslayer Greatsword", "Arstor's Spear"] + ), + ("Soul of a Crystal Sage", "Sage", ["Crystal Sage's Rapier", "Crystal Hail"]), + ("Soul of the Deacons of the Deep", "Deacons", ["Cleric's Candlestick", "Deep Soul"]), + ("Soul of a Stray Demon", "Stray Demon", ["Havel's Ring", "Boulder Heave"]), + ( + "Soul of the Blood of the Wolf", "Abyss Watchers", + ["Farron Greatsword", "Wolf Knight's Greatsword"] + ), + ("Soul of High Lord Wolnir", "Wolnir", ["Wolnir's Holy Sword", "Black Serpent"]), + ("Soul of a Demon", "Fire Demon", ["Demon's Greataxe", "Demon's Fist"]), + ( + "Soul of the Old Demon King", "Old Demon King", + ["Old King's Great Hammer", "Chaos Bed Vestiges"] + ), + ( + "Soul of Pontiff Sulyvahn", "Pontiff", + ["Greatsword of Judgment", "Profaned Greatsword"] + ), + ("Soul of Yhorm the Giant", "Yhorm", ["Yhorm's Great Machete", "Yhorm's Greatshield"]), + ("Soul of the Dancer", "Dancer", ["Dancer's Enchanted Swords", "Soothing Sunlight"]), + ( + "Soul of Dragonslayer Armour", "Dragonslayer", + ["Dragonslayer Greataxe", "Dragonslayer Greatshield"] + ), + ( + "Soul of Consumed Oceiros", "Oceiros", + ["Moonlight Greatsword", "White Dragon Breath"] + ), + ( + "Soul of the Twin Princes", "Princes", + ["Lorian's Greatsword", "Lothric's Holy Sword"] + ), + ("Soul of Champion Gundyr", "Champion", ["Gundyr's Halberd", "Prisoner's Chain"]), + ( + "Soul of the Nameless King", "Nameless", + ["Storm Curved Sword", "Dragonslayer Swordspear", "Lightning Storm"] + ), + ("Soul of the Lords", "Cinder", ["Firelink Greatsword", "Sunlight Spear"]), + ("Soul of Sister Friede", "Friede", ["Friede's Great Scythe", "Rose of Ariandel"]), + ("Soul of the Demon Prince", "Demon Prince", ["Demon's Scar", "Seething Chaos"]), + ("Soul of Darkeater Midir", "Midir", ["Frayed Blade", "Old Moonlight"]), + ("Soul of Slave Knight Gael", "Gael", ["Gael's Greatsword", "Repeating Crossbow"]), + ] + for (soul, soul_name, items) in transpositions: + self._add_location_rule([ + f"FS: {item} - Ludleth for {soul_name}" for item in items + ], lambda state, s=soul: ( + state.has(s, self.player) and state.has("Transposing Kiln", self.player) + )) + + def _add_crow_rules(self) -> None: + """Adds rules for items obtainable by trading items to the crow on Firelink roof.""" + + crow = { + "Loretta's Bone": "Ring of Sacrifice", + # "Avelyn": "Titanite Scale", # Missing from static randomizer + "Coiled Sword Fragment": "Titanite Slab", + "Seed of a Giant Tree": "Iron Leggings", + "Siegbräu": "Armor of the Sun", + # Static randomizer can't randomize Hodrick's drop yet + # "Vertebra Shackle": "Lucatiel's Mask", + "Xanthous Crown": "Lightning Gem", + "Mendicant's Staff": "Sunlight Shield", + "Blacksmith Hammer": "Titanite Scale", + "Large Leather Shield": "Twinkling Titanite", + "Moaning Shield": "Blessed Gem", + "Eleonora": "Hollow Gem", + } + for (given, received) in crow.items(): + name = f"FSBT: {received} - crow for {given}" + self._add_location_rule(name, given) + + # Don't let crow items have foreign items because they're picked up in a way that's + # missed by the hook we use to send location items + self._add_item_rule(name, lambda item: ( + item.player == self.player + # Because of the weird way they're delivered, crow items don't seem to support + # infused or upgraded weapons. + and not item.data.is_infused + and not item.data.is_upgraded + )) + + def _add_allow_useful_location_rules(self) -> None: + """Adds rules for locations that can contain useful but not necessary items. + + If we allow useful items in the excluded locations, we don't want Archipelago's fill + algorithm to consider them excluded because it never allows useful items there. Instead, we + manually add item rules to exclude important items. + """ + + all_locations = self._get_our_locations() + + allow_useful_locations = ( + ( + { + location.name + for location in all_locations + if location.name in self.all_excluded_locations + and not location.data.missable + } + if self.options.excluded_location_behavior < self.options.missable_location_behavior + else self.all_excluded_locations + ) + if self.options.excluded_location_behavior == "allow_useful" + else set() + ).union( + { + location.name + for location in all_locations + if location.data.missable + and not ( + location.name in self.all_excluded_locations + and self.options.missable_location_behavior < + self.options.excluded_location_behavior + ) + } + if self.options.missable_location_behavior == "allow_useful" + else set() + ) + for location in allow_useful_locations: + self._add_item_rule( + location, + lambda item: not item.advancement + ) + + if self.options.excluded_location_behavior == "allow_useful": + self.options.exclude_locations.value.clear() + + def _add_early_item_rules(self, randomized_items: Set[str]) -> None: + """Adds rules to make sure specific items are available early.""" + + if "Pyromancy Flame" in randomized_items: + # Make this available early because so many items are useless without it. + self._add_entrance_rule("Road of Sacrifices", "Pyromancy Flame") + self._add_entrance_rule("Consumed King's Garden", "Pyromancy Flame") + self._add_entrance_rule("Grand Archives", "Pyromancy Flame") + if "Transposing Kiln" in randomized_items: + # Make this available early so players can make use of their boss souls. + self._add_entrance_rule("Road of Sacrifices", "Transposing Kiln") + self._add_entrance_rule("Consumed King's Garden", "Transposing Kiln") + self._add_entrance_rule("Grand Archives", "Transposing Kiln") + # Make this available pretty early + if "Small Lothric Banner" in randomized_items: + if self.options.early_banner == "early_global": + self.multiworld.early_items[self.player]["Small Lothric Banner"] = 1 + elif self.options.early_banner == "early_local": + self.multiworld.local_early_items[self.player]["Small Lothric Banner"] = 1 + + def _has_any_scroll(self, state: CollectionState) -> bool: + """Returns whether the given state has any scroll item.""" + return ( + state.has("Sage's Scroll", self.player) + or state.has("Golden Scroll", self.player) + or state.has("Logan's Scroll", self.player) + or state.has("Crystal Scroll", self.player) + ) + + def _add_location_rule(self, location: Union[str, List[str]], rule: Union[CollectionRule, str]) -> None: + """Sets a rule for the given location if it that location is randomized. + + The rule can just be a single item/event name as well as an explicit rule lambda. + """ + locations = location if isinstance(location, list) else [location] + for location in locations: + data = location_dictionary[location] + if data.dlc and not self.options.enable_dlc: return + if data.ngp and not self.options.enable_ngp: return + + if not self._is_location_available(location): return + if isinstance(rule, str): + assert item_dictionary[rule].classification == ItemClassification.progression + rule = lambda state, item=rule: state.has(item, self.player) + add_rule(self.multiworld.get_location(location, self.player), rule) + + def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: + """Sets a rule for the entrance to the given region.""" + assert region in location_tables + if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if isinstance(rule, str): + if " -> " not in rule: + assert item_dictionary[rule].classification == ItemClassification.progression + rule = lambda state, item=rule: state.has(item, self.player) + add_rule(self.multiworld.get_entrance("Go To " + region, self.player), rule) + + def _add_item_rule(self, location: str, rule: ItemRule) -> None: + """Sets a rule for what items are allowed in a given location.""" + if not self._is_location_available(location): return + add_item_rule(self.multiworld.get_location(location, self.player), rule) + + def _can_go_to(self, state, region) -> bool: + """Returns whether state can access the given region name.""" + return state.can_reach_entrance(f"Go To {region}", self.player) + + def _can_get(self, state, location) -> bool: + """Returns whether state can access the given location name.""" + return state.can_reach_location(location, self.player) + + def _is_location_available( + self, + location: Union[str, DS3LocationData, DarkSouls3Location] + ) -> bool: + """Returns whether the given location is being randomized.""" + if isinstance(location, DS3LocationData): + data = location + elif isinstance(location, DarkSouls3Location): + data = location.data + else: + data = location_dictionary[location] + + return ( + not data.is_event + and (not data.dlc or bool(self.options.enable_dlc)) + and (not data.ngp or bool(self.options.enable_ngp)) + and not ( + self.options.excluded_location_behavior == "do_not_randomize" + and data.name in self.all_excluded_locations + ) + and not ( + self.options.missable_location_behavior == "do_not_randomize" + and data.missable + ) + ) + + def write_spoiler(self, spoiler_handle: TextIO) -> None: + text = "" + + if self.yhorm_location != default_yhorm_location: + text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n" + + if self.options.excluded_location_behavior == "allow_useful": + text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n" + + if text: + text = "\n" + text + "\n" + spoiler_handle.write(text) + + def post_fill(self): + """If item smoothing is enabled, rearrange items so they scale up smoothly through the run. + + This determines the approximate order a given silo of items (say, soul items) show up in the + main game, then rearranges their shuffled placements to match that order. It determines what + should come "earlier" or "later" based on sphere order: earlier spheres get lower-level + items, later spheres get higher-level ones. Within a sphere, items in DS3 are distributed in + region order, and then the best items in a sphere go into the multiworld. + """ + + locations_by_sphere = [ + sorted(loc for loc in sphere if loc.item.player == self.player and not loc.locked) + for sphere in self.multiworld.get_spheres() + ] + + # All items in the base game in approximately the order they appear + all_item_order: List[DS3ItemData] = [ + item_dictionary[location.default_item_name] + for region in region_order + # Shuffle locations within each region. + for location in self._shuffle(location_tables[region]) + if self._is_location_available(location) + ] + + # All DarkSouls3Items for this world that have been assigned anywhere, grouped by name + full_items_by_name: Dict[str, List[DarkSouls3Item]] = defaultdict(list) + for location in self.multiworld.get_filled_locations(): + if location.item.player == self.player and ( + location.player != self.player or self._is_location_available(location) + ): + full_items_by_name[location.item.name].append(location.item) + + def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None: + """Rearrange all items in item_order to match that order. + + Note: this requires that item_order exactly matches the number of placed items from this + world matching the given names. + """ + + # Convert items to full DarkSouls3Items. + converted_item_order: List[DarkSouls3Item] = [ + item for item in ( + ( + # full_items_by_name won't contain DLC items if the DLC is disabled. + (full_items_by_name[item.name] or [None]).pop(0) + if isinstance(item, DS3ItemData) else item + ) + for item in item_order + ) + # Never re-order event items, because they weren't randomized in the first place. + if item and item.code is not None + ] + + names = {item.name for item in converted_item_order} + + all_matching_locations = [ + loc + for sphere in locations_by_sphere + for loc in sphere + if loc.item.name in names + ] + + # It's expected that there may be more total items than there are matching locations if + # the player has chosen a more limited accessibility option, since the matching + # locations *only* include items in the spheres of accessibility. + if len(converted_item_order) < len(all_matching_locations): + raise Exception( + f"DS3 bug: there are {len(all_matching_locations)} locations that can " + + f"contain smoothed items, but only {len(converted_item_order)} items to smooth." + ) + for sphere in locations_by_sphere: + locations = [loc for loc in sphere if loc.item.name in names] + + # Check the game, not the player, because we know how to sort within regions for DS3 + offworld = self._shuffle([loc for loc in locations if loc.game != "Dark Souls III"]) + onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"), + key=lambda loc: loc.data.region_value) + + # Give offworld regions the last (best) items within a given sphere + for location in onworld + offworld: + new_item = self._pop_item(location, converted_item_order) + location.item = new_item + new_item.location = location + + if self.options.smooth_upgrade_items: + base_names = { + "Titanite Shard", "Large Titanite Shard", "Titanite Chunk", "Titanite Slab", + "Titanite Scale", "Twinkling Titanite", "Farron Coal", "Sage's Coal", "Giant's Coal", + "Profaned Coal" + } + smooth_items([item for item in all_item_order if item.base_name in base_names]) + + if self.options.smooth_soul_items: + smooth_items([ + item for item in all_item_order + if item.souls and item.classification != ItemClassification.progression + ]) + + if self.options.smooth_upgraded_weapons: + upgraded_weapons = [ + location.item + for location in self.multiworld.get_filled_locations() + if location.item.player == self.player + and location.item.level and location.item.level > 0 + and location.item.classification != ItemClassification.progression + ] + upgraded_weapons.sort(key=lambda item: item.level) + smooth_items(upgraded_weapons) + + def _shuffle(self, seq: Sequence) -> List: + """Returns a shuffled copy of a sequence.""" + copy = list(seq) + self.random.shuffle(copy) + return copy + + def _pop_item( + self, + location: Location, + items: List[DarkSouls3Item] + ) -> DarkSouls3Item: + """Returns the next item in items that can be assigned to location.""" + for i, item in enumerate(items): + if location.can_fill(self.multiworld.state, item, False): + return items.pop(i) + + # If we can't find a suitable item, give up and assign an unsuitable one. + return items.pop(0) + + def _get_our_locations(self) -> List[DarkSouls3Location]: + return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player)) def fill_slot_data(self) -> Dict[str, object]: slot_data: Dict[str, object] = {} - # Depending on the specified option, modify items hexadecimal value to add an upgrade level or infusion - name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()} - - # Randomize some weapon upgrades - if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none: - # if the user made an error and set a min higher than the max we default to the max - max_5 = self.multiworld.max_levels_in_5[self.player] - min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5) - max_10 = self.multiworld.max_levels_in_10[self.player] - min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10) - weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player] - - for item in item_dictionary.values(): - if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage: - if item.category == DS3ItemCategory.WEAPON_UPGRADE_5: - name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5) - elif item.category in {DS3ItemCategory.WEAPON_UPGRADE_10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE}: - name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10) - - # Randomize some weapon infusions - if self.multiworld.randomize_infusion[self.player] == Toggle.option_true: - infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player] - for item in item_dictionary.values(): - if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}: - if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage: - name_to_ds3_code[item.name] += 100 * self.multiworld.per_slot_randoms[self.player].randint(0, 15) - - # Create the mandatory lists to generate the player's output file - items_id = [] - items_address = [] - locations_id = [] - locations_address = [] - locations_target = [] - for location in self.multiworld.get_filled_locations(): - # Skip events - if location.item.code is None: - continue - - if location.item.player == self.player: - items_id.append(location.item.code) - items_address.append(name_to_ds3_code[location.item.name]) - - if location.player == self.player: - locations_address.append(item_dictionary[location_dictionary[location.name].default_item].ds3_code) - locations_id.append(location.address) - if location.item.player == self.player: - locations_target.append(name_to_ds3_code[location.item.name]) - else: - locations_target.append(0) + # Once all clients support overlapping item IDs, adjust the DS3 AP item IDs to encode the + # in-game ID as well as the count so that we don't need to send this information at all. + # + # We include all the items the game knows about so that users can manually request items + # that aren't randomized, and then we _also_ include all the items that are placed in + # practice `item_dictionary.values()` doesn't include upgraded or infused weapons. + all_items = { + cast(DarkSouls3Item, location.item).data + for location in self.multiworld.get_filled_locations() + # item.code None is used for events, which we want to skip + if location.item.code is not None and location.item.player == self.player + }.union(item_dictionary.values()) + + ap_ids_to_ds3_ids: Dict[str, int] = {} + item_counts: Dict[str, int] = {} + for item in all_items: + if item.ap_code is None: continue + if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code + if item.count != 1: item_counts[str(item.ap_code)] = item.count + + # A map from Archipelago's location IDs to the keys the static randomizer uses to identify + # locations. + location_ids_to_keys: Dict[int, str] = {} + for location in cast(List[DarkSouls3Location], self.multiworld.get_filled_locations(self.player)): + # Skip events and only look at this world's locations + if (location.address is not None and location.item.code is not None + and location.data.static): + location_ids_to_keys[location.address] = location.data.static slot_data = { "options": { - "enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value, - "enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value, - "enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value, - "enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value, - "enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value, - "enable_key_locations": self.multiworld.enable_key_locations[self.player].value, - "enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value, - "enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value, - "enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value, - "auto_equip": self.multiworld.auto_equip[self.player].value, - "lock_equip": self.multiworld.lock_equip[self.player].value, - "no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value, - "death_link": self.multiworld.death_link[self.player].value, - "no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value, - "no_equip_load": self.multiworld.no_equip_load[self.player].value, - "enable_dlc": self.multiworld.enable_dlc[self.player].value + "random_starting_loadout": self.options.random_starting_loadout.value, + "require_one_handed_starting_weapons": self.options.require_one_handed_starting_weapons.value, + "auto_equip": self.options.auto_equip.value, + "lock_equip": self.options.lock_equip.value, + "no_weapon_requirements": self.options.no_weapon_requirements.value, + "death_link": self.options.death_link.value, + "no_spell_requirements": self.options.no_spell_requirements.value, + "no_equip_load": self.options.no_equip_load.value, + "enable_dlc": self.options.enable_dlc.value, + "enable_ngp": self.options.enable_ngp.value, + "smooth_soul_locations": self.options.smooth_soul_items.value, + "smooth_upgrade_locations": self.options.smooth_upgrade_items.value, + "randomize_enemies": self.options.randomize_enemies.value, + "randomize_mimics_with_enemies": self.options.randomize_mimics_with_enemies.value, + "randomize_small_crystal_lizards_with_enemies": self.options.randomize_small_crystal_lizards_with_enemies.value, + "reduce_harmless_enemies": self.options.reduce_harmless_enemies.value, + "simple_early_bosses": self.options.simple_early_bosses.value, + "scale_enemies": self.options.scale_enemies.value, + "all_chests_are_mimics": self.options.all_chests_are_mimics.value, + "impatient_mimics": self.options.impatient_mimics.value, }, "seed": self.multiworld.seed_name, # to verify the server's multiworld "slot": self.multiworld.player_name[self.player], # to connect to server - "base_id": self.base_id, # to merge location and items lists - "locationsId": locations_id, - "locationsAddress": locations_address, - "locationsTarget": locations_target, - "itemsId": items_id, - "itemsAddress": items_address + # Reserializing here is silly, but it's easier for the static randomizer. + "random_enemy_preset": json.dumps(self.options.random_enemy_preset.value), + "yhorm": ( + f"{self.yhorm_location.name} {self.yhorm_location.id}" + if self.yhorm_location != default_yhorm_location + else None + ), + "apIdsToItemIds": ap_ids_to_ds3_ids, + "itemCounts": item_counts, + "locationIdsToKeys": location_ids_to_keys, } return slot_data + + @staticmethod + def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: + return slot_data diff --git a/worlds/dark_souls_3/detailed_location_descriptions.py b/worlds/dark_souls_3/detailed_location_descriptions.py new file mode 100644 index 000000000000..e20c700ab1bc --- /dev/null +++ b/worlds/dark_souls_3/detailed_location_descriptions.py @@ -0,0 +1,97 @@ +# python -m worlds.dark_souls_3.detailed_location_descriptions \ +# worlds/dark_souls_3/detailed_location_descriptions.py +# +# This script downloads the static randomizer's descriptions for each location and adds them to +# the location documentation. + +from collections import defaultdict +import html +import os +import re +import requests +import yaml + +from .Locations import location_dictionary + + +location_re = re.compile(r'^([A-Z0-9]+): (.*?)(?:$| - )') + +if __name__ == '__main__': + # TODO: update this to the main branch of the main randomizer once Archipelago support is merged + url = 'https://raw.githubusercontent.com/nex3/SoulsRandomizers/archipelago-server/dist/Base/annotations.txt' + response = requests.get(url) + if response.status_code != 200: + raise Exception(f"Got {response.status_code} when downloading static randomizer locations") + annotations = yaml.load(response.text, Loader=yaml.Loader) + + static_to_archi_regions = { + area['Name']: area['Archipelago'] + for area in annotations['Areas'] + } + + descriptions_by_key = {slot['Key']: slot['Text'] for slot in annotations['Slots']} + + # A map from (region, item name) pairs to all the descriptions that match those pairs. + descriptions_by_location = defaultdict(list) + + # A map from item names to all the descriptions for those item names. + descriptions_by_item = defaultdict(list) + + for slot in annotations['Slots']: + region = static_to_archi_regions[slot['Area']] + for item in slot['DebugText']: + name = item.split(" - ")[0] + descriptions_by_location[(region, name)].append(slot['Text']) + descriptions_by_item[name].append(slot['Text']) + counts_by_location = { + location: len(descriptions) for (location, descriptions) in descriptions_by_location.items() + } + + location_names_to_descriptions = {} + for location in location_dictionary.values(): + if location.ap_code is None: continue + if location.static: + location_names_to_descriptions[location.name] = descriptions_by_key[location.static] + continue + + match = location_re.match(location.name) + if not match: + raise Exception(f"Location name \"{location.name}\" doesn't match expected format.") + + item_candidates = descriptions_by_item[match[2]] + if len(item_candidates) == 1: + location_names_to_descriptions[location.name] = item_candidates[0] + continue + + key = (match[1], match[2]) + if key not in descriptions_by_location: + raise Exception(f'No static randomizer location found matching "{match[1]}: {match[2]}".') + + candidates = descriptions_by_location[key] + if len(candidates) == 0: + raise Exception( + f'There are only {counts_by_location[key]} locations in the static randomizer ' + + f'matching "{match[1]}: {match[2]}", but there are more in Archipelago.' + ) + + location_names_to_descriptions[location.name] = candidates.pop(0) + + table = "\n" + for (name, description) in sorted( + location_names_to_descriptions.items(), + key = lambda pair: pair[0] + ): + table += f"\n" + table += "
Location nameDetailed description
{html.escape(name)}{html.escape(description)}
\n" + + with open(os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), 'r+') as f: + original = f.read() + start_flag = "\n" + start = original.index(start_flag) + len(start_flag) + end = original.index("") + + f.seek(0) + f.write(original[:start] + table + original[end:]) + f.truncate() + + print("Updated docs/locations_en.md!") diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index f31358bb9c2f..06227226aafe 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,28 +1,201 @@ # Dark Souls III +Game Page | [Items] | [Locations] + +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## What do I need to do to randomize DS3? + +See full instructions on [the setup page]. + +[the setup page]: /tutorial/Dark%20Souls%20III/setup/en + ## Where is the options page? -The [player options page for this game](../player-options) contains all the options you need to configure and export a -config file. +The [player options page for this game][options] contains all the options you +need to configure and export a config file. + +[options]: ../player-options ## What does randomization do to this game? -Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be -randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the -location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what -happens when you randomize Estus Shards and Undead Bone Shards. +1. All item locations are randomized, including those in the overworld, in + shops, and dropped by enemies. Most locations can contain games from other + worlds, and any items from your world can appear in other players' worlds. + +2. By default, all enemies and bosses are randomized. This can be disabled by + setting "Randomize Enemies" to false. + +3. By default, the starting equipment for each class is randomized. This can be + disabled by setting "Randomize Starting Loadout" to false. + +4. By setting the "Randomize Weapon Level" or "Randomize Infusion" options, you + can randomize whether the weapons you find will be upgraded or infused. + +There are also options that can make playing the game more convenient or +bring a new experience, like removing equip loads or auto-equipping weapons as +you pick them up. Check out [the options page][options] for more! + +## What's the goal? + +Your goal is to find the four "Cinders of a Lord" items randomized into the +multiworld and defeat the boss in the Kiln of the First Flame. + +## Do I have to check every item in every area? + +Dark Souls III has about 1500 item locations, which is a lot of checks for a +single run! But you don't necessarily need to check all of them. Locations that +you can potentially miss, such as rewards for failable quests or soul +transposition items, will _never_ have items required for any game to progress. +The following types of locations are also guaranteed not to contain progression +items by default: + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +You can customize which locations are guaranteed not to contain progression +items by setting the `exclude_locations` field in your YAML to the [location +groups] you want to omit. For example, this is the default setting but without +"Hidden" so that hidden locations can contain progression items: + +[location groups]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + +```yaml +Dark Souls III: + exclude_locations: + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +This allows _all_ non-missable locations to have progression items, if you're in +for the long haul: + +```yaml +Dark Souls III: + exclude_locations: [] +``` + +## What if I don't want to do the whole game? + +If you want a shorter DS3 randomizer experience, you can exclude entire regions +from containing progression items. The items and enemies from those regions will +still be included in the randomization pool, but none of them will be mandatory. +For example, the following configuration just requires you to play the game +through Irithyll of the Boreal Valley: + +```yaml +Dark Souls III: + # Enable the DLC so it's included in the randomization pool + enable_dlc: true + + exclude_locations: + # Exclude late-game and DLC regions + - Anor Londo + - Lothric Castle + - Consumed King's Garden + - Untended Graves + - Grand Archives + - Archdragon Peak + - Painted World of Ariandel + - Dreg Heap + - Ringed City + + # Default exclusions + - Hidden + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +## Where can I learn more about Dark Souls III locations? + +Location names have to pack a lot of information into very little space. To +better understand them, check out the [location guide], which explains all the +names used in locations and provides more detailed descriptions for each +individual location. + +[location guide]: /tutorial/Dark%20Souls%20III/locations/en + +## Where can I learn more about Dark Souls III items? + +Check out the [item guide], which explains the named groups available for items. + +[item guide]: /tutorial/Dark%20Souls%20III/items/en + +## What's new from 2.x.x? + +Version 3.0.0 of the Dark Souls III Archipelago client has a number of +substantial differences with the older 2.x.x versions. Improvements include: + +* Support for randomizing all item locations, not just unique items. + +* Support for randomizing items in shops, starting loadouts, Path of the Dragon, + and more. + +* Built-in integration with the enemy randomizer, including consistent seeding + for races. + +* Support for the latest patch for Dark Souls III, 1.15.2. Older patches are + *not* supported. + +* Optional smooth distribution for upgrade items, upgraded weapons, and soul + items so you're more likely to see weaker items earlier and more powerful + items later. + +* More detailed location names that indicate where a location is, not just what + it replaces. + +* Other players' item names are visible in DS3. + +* If you pick up items while static, they'll still send once you reconnect. + +However, 2.x.x YAMLs are not compatible with 3.0.0. You'll need to [generate a +new YAML configuration] for use with 3.x.x. + +[generating a new YAML configuration]: /games/Dark%20Souls%20III/player-options + +The following options have been removed: + +* `enable_boss_locations` is now controlled by the `soul_locations` option. + +* `enable_progressive_locations` was removed because all locations are now + individually randomized rather than replaced with a progressive list. + +* `pool_type` has been removed. Since there are no longer any non-randomized + items in randomized categories, there's not a meaningful distinction between + "shuffle" and "various" mode. -It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have -one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as -removing weapon requirements or auto-equipping whatever equipment you most recently received. +* `enable_*_locations` options have all been removed. Instead, you can now add + [location group names] to the `exclude_locations` option to prevent them from + containing important items. -The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. + [location group names]: /tutorial/Dark%20Souls%20III/locations/en#location-groups -## What Dark Souls III items can appear in other players' worlds? + By default, the Hidden, Small Crystal Lizards, Upgrade, Small Souls, and + Miscellaneous groups are in `exclude_locations`. Once you've chosen your + excluded locations, you can set `excluded_locations: unrandomized` to preserve + the default vanilla item placements for all excluded locations. -Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables, -spells, upgrade materials, etc... +* `guaranteed_items`: In almost all cases, all items from the base game are now + included somewhere in the multiworld. -## What does another world's item look like in Dark Souls III? +In addition, the following options have changed: -In Dark Souls III, items which are sent to other worlds appear as Prism Stones. +* The location names used in options like `exclude_locations` have changed. See + the [location guide] for a full description. diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md new file mode 100644 index 000000000000..b9de5e500a96 --- /dev/null +++ b/worlds/dark_souls_3/docs/items_en.md @@ -0,0 +1,24 @@ +# Dark Souls III Items + +[Game Page] | Items | [Locations] + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## Item Groups + +The Dark Souls III randomizer supports a number of item group names, which can +be used in YAML options like `local_items` to refer to many items at once: + +* **Progression:** Items which unlock locations. +* **Cinders:** All four Cinders of a Lord. Once you have these four, you can + fight Soul of Cinder and win the game. +* **Miscellaneous:** Generic stackable items, such as arrows, firebombs, buffs, + and so on. +* **Unique:** Items that are unique per NG cycle, such as scrolls, keys, ashes, + and so on. Doesn't include equipment, spells, or souls. +* **Boss Souls:** Souls that can be traded with Ludleth, including Soul of + Rosaria. +* **Small Souls:** Soul items, not including boss souls. +* **Upgrade:** Upgrade items, including titanite, gems, and Shriving Stones. +* **Healing:** Undead Bone Shards and Estus Shards. diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md new file mode 100644 index 000000000000..ef07b84b2b34 --- /dev/null +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -0,0 +1,2276 @@ +# Dark Souls III Locations + +[Game Page] | [Items] | Locations + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Items]: /tutorial/Dark%20Souls%20III/items/en + +## Table of Contents + +* [Location Groups](#location-groups) +* [Understanding Location Names](#understanding-location-names) + * [HWL: High Wall of Lothric](#high-wall-of-lothric) + * [US: Undead Settlement](#undead-settlement) + * [RS: Road of Sacrifices](#road-of-sacrifices) + * [CD: Cathedral of the Deep](#cathedral-of-the-deep) + * [FK: Farron Keep](#farron-keep) + * [CC: Catacombs of Carthus](#catacombs-of-carthus) + * [SL: Smouldering Lake](#smouldering-lake) + * [IBV: Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) + * [ID: Irithyll Dungeon](#irithyll-dungeon) + * [PC: Profaned Capital](#profaned-capital) + * [AL: Anor Londo](#anor-londo) + * [LC: Lothric Castle](#lothric-castle) + * [CKG: Consumed King's Garden](#consumed-kings-garden) + * [GA: Grand Archives](#grand-archives) + * [UG: Untended Graves](#untended-graves) + * [AP: Archdragon Peak](#archdragon-peak) + * [PW1: Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) + * [PW2: Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) + * [DH: Dreg Heap](#dreg-heap) + * [RC: Ringed City](#ringed-city) +* [Detailed Location Descriptions](#detailed-location-descriptions) + +## Location Groups + +The Dark Souls III randomizer supports a number of location group names, which +can be used in YAML options like `exclude_locations` to refer to many locations +at once: + +* **Prominent:** A small number of locations that are in very obvious locations. + Mostly boss drops. Ideal for setting as priority locations. + +* **Progression:** Locations that contain items in vanilla which unlock other + locations. + +* **Boss Rewards:** Boss drops. Does not include soul transfusions or shop + items. + +* **Miniboss Rewards:** Miniboss drops. Minibosses are large enemies that don't + respawn after being killed and usually drop some sort of treasure, such as + Boreal Outrider Knights and Ravenous Crystal Lizards. Only includes enemies + considered minibosses by the enemy randomizer. + +* **Mimic Rewards:** Drops from enemies that are mimics in vanilla. + +* **Hostile NPC Rewards:** Drops from NPCs that are hostile to you. This + includes scripted invaders and initially-friendly NPCs that must be fought as + part of their quest. + +* **Friendly NPC Rewards:** Items given by friendly NPCs as part of their quests + or from non-violent interaction. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Boss Souls:** Locations that contain boss souls in vanilla, as well as Soul + of Rosaria. + +* **Unique:** Locations that contain items in vanilla that are unique per NG + cycle, such as scrolls, keys, ashes, and so on. Doesn't cover equipment, + spells, or souls. + +* **Healing:** Locations that contain Undead Bone Shards and Estus Shards in + vanilla. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Weapons:** Locations that contain weapons in vanilla. + +* **Shields:** Locations that contain shields in vanilla. + +* **Armor:** Locations that contain armor in vanilla. + +* **Rings:** Locations that contain rings in vanilla. + +* **Spells:** Locations that contain spells in vanilla. + +## Understanding Location Names + +All locations begin with an abbreviation indicating their general region. Most +locations have a set of landmarks that are used in location names to keep them +short. + +* **FS:** Firelink Shrine +* **FSBT:** Firelink Shrine belltower +* **HWL:** [High Wall of Lothric](#high-wall-of-lothric) +* **US:** [Undead Settlement](#undead-settlement) +* **RS:** [Road of Sacrifices](#road-of-sacrifices) +* **CD:** [Cathedral of the Deep](#cathedral-of-the-deep) +* **FK:** [Farron Keep](#farron-keep) +* **CC:** [Catacombs of Carthus](#catacombs-of-carthus) +* **SL:** [Smouldering Lake](#smouldering-lake) +* **IBV:** [Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) +* **ID:** [Irithyll Dungeon](#irithyll-dungeon) +* **PC:** [Profaned Capital](#profaned-capital) +* **AL:** [Anor Londo](#anor-londo) +* **LC:** [Lothric Castle](#lothric-castle) +* **CKG:** [Consumed King's Garden](#consumed-kings-garden) +* **GA:** [Grand Archives](#grand-archives) +* **UG:** [Untended Graves](#untended-graves) +* **AP:** [Archdragon Peak](#archdragon-peak) +* **PW1:** [Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) +* **PW2:** [Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) +* **DH:** [Dreg Heap](#dreg-heap) +* **RC:** [Ringed City](#ringed-city) + +General notes: + +* "Lizard" always refers to a small crystal lizard. + +* "Miniboss" are large enemies that don't respawn after being killed and usually + drop some sort of treasure, such as Boreal Outrider Knights and Ravenous + Crystal Lizards. + +* NPC quest items are always in the first location you can get them _without_ + killing the NPC or ending the quest early. + +### High Wall of Lothric + +* **Back tower:** The tower _behind_ the High Wall of Lothric bonfire, past the + path to the shortcut elevator. + +* **Corpse tower:** The first tower after the High Wall of Lothric bonfire, with + a dead Wyvern on top of it. + +* **Fire tower:** The second tower after the High Wall of Lothric bonfire, where + a living Wyvern lands and breathes fire at you. + +* **Flame plaza:** The open area with many items where the Wyvern breathes fire. + +* **Wall tower:** The third tower after the High Wall of Lothric bonfire, with + the Tower on the Wall bonfire. + +* **Fort:** The large building after the Tower on the Wall bonfire, with the + transforming hollow on top. + + * "Entry": The first room you enter after descending the ladder from the roof. + + * "Walkway": The top floor of the tall room, with a path around the edge + hidden by a large wheel. + + * "Mezzanine": The middle floor of the tall room, with a chest. + + * "Ground": The bottom floor of the tall room, with an anvil and many mobs. + +* **Fountain:** The large fountain with many dead knights around it, where the + Winged Knight patrols in vanilla. + +* **Shortcut:** The unlockable path between the promenade and the High Wall of + Lothric bonfire, including both the elevator and the area at its base. + +* **Promenade:** The long, wide path between the two boss arenas. + +### Undead Settlement + +* **Foot:** The area where you first appear, around the Foot of the High Wall + bonfire. + +* **Burning tree:** The tree near the beginning of the region, with the + Cathedral Evangelist in front of it in vanilla. + +* **Hanging corpse room:** The dark room to the left of the burning tree with + many hanging corpses inside, on the way to the Dilapidated Bridge bonfire. + +* **Back alley:** The path between buildings leading to the Dilapidated Bridge + bonfire. + +* **Stable:** The building complex across the bridge to the right of the burning + tree. + +* **White tree:** The birch tree by the Dilapidated Bridge bonfire, where the + giant shoots arrows. + +* **Sewer:** The underground passage between the chasm and the Dilapidated + Bridge bonfire. + +* **Chasm:** The chasm underneath the bridge on the way to the tower. It's + possible to get into the chasm without a key by dropping down next to Eygon of + Carim with a full health bar. + +* **Tower:** The tower at the end of the region with the giant archer at the + top. + +* **Tower village:** The village reachable from the tower, where the Fire Demon + patrols in vanilla. + +### Road of Sacrifices + +The area after the Crystal Sage is considered part of the Cathedral of the Deep +region. + +* **Road:** The path from the Road of Sacrifices bonfire to the Halfway Fortress + bonfire. + +* **Woods:** The wooded area on land, after the Halfway Fortress bonfire and + surrounding the Crucifixion Woods bonfire. + +* **Water:** The watery area, covered in crabs in vanilla. + +* **Deep water:** The area in the water near the ladder to Farron Keep, where + your walking is slowed. + +* **Stronghold:** The stone building complex on the way to Crystal Sage. + + * "Left room" is the room whose entrance is near the Crucifixion Woods + bonfire. + + * "Right room" is the room up the stairs closer to Farron Keep. + +* **Keep perimeter:** The building with the Black Knight and the locked door to + the Farron Keep Perimeter bonfire. + +### Cathedral of the Deep + +* **Path:** The path from Road of Sacrifices to the cathedral proper. + +* **Moat:** The circular path around the base of the front of the + cathedral, with the Ravenous Crystal Lizard and Corpse-Grubs in vanilla. + +* **Graveyard:** The area with respawning enemies up the hill from the Cleansing + Chapel bonfire. + +* **White tree:** The birch tree below the front doors of the chapel and across + the moat from the graveyard, where the giant shoots arrows if he's still + alive. + +* **Lower roofs:** The roofs, flying buttresses, and associated areas to the + right of the front door, which must be traversed before entering the + cathedral. + +* **Upper roofs:** The roofs, flying buttresses, and rafters leading to the + Rosaria's Bedchamber bonfire. + +* **Main hall:** The central and largest room in the cathedral, with the muck + that slows your movement. Divided into the south (with the sleeping giant in + vanilla) and east (with many items) wings, with north pointing towards the + door to the boss. + +* **Side chapel:** The room with rows of pews and the patrolling Cathedral + Knight in vanilla, to the side of the main hall. + +### Farron Keep + +* **Left island:** The large island with the ritual flame, to the left as you + leave the Farron Keep bonfire. + +* **Right island:** The large island with the ritual flame, to the right as you + leave the Farron Keep bonfire. + +* **Hidden cave:** A small cave in the far corner of the map, closest to the + right island. Near a bunch of basilisks in vanilla. + +* **Keep ruins:** The following two islands: + + * "Bonfire island": The island with the Keep Ruins bonfire. + * "Ritual island": The island with one of the three ritual fires. + +* **White tree**: The birch tree by the ramp down from the keep ruins bonfire + island, where the giant shoots arrows if he's still alive. + +* **Keep proper:** The building with the Old Wolf of Farron bonfire. + +* **Upper keep:** The area on top of the keep proper, reachable from the + elevator from the Old Wolf of Farron bonfire. + +* **Perimeter:** The area from near the Farron Keep Perimeter bonfire, including + the stone building and the path to the boss. + +### Catacombs of Carthus + +All the area up to the Small Doll wall into Irithyll is considered part of the +Catacombs of Carthus region. + +* **Atrium:** The large open area you first enter and the rooms attached to it. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the short stairs but at the top of the long + stairway that the skeleton ball rolls down. + +* **Crypt:** The enclosed area at the bottom of the long stairway that the + skeleton ball rolls down. + + * "Upper" is the floor the long stairway leads to that also contains the + Catacombs of Carthus bonfire. + * "Lower" is the floor with rats and bonewheels in vanilla. + * "Across" is the area reached by going up the set of stairs across from + the entrance downstairs. + +* **Cavern:** The even larger open area past the crypt with the rope bridge to + the boss arena. + +* **Tomb:** The area on the way to Smouldering Lake, reachable by cutting down + the rope bridge and climbing down it. + +* **Irithyll Bridge:** The outdoor bridge leading to Irithyll of the Boreal + Valley. + +### Smouldering Lake + +* **Lake:** The watery area you enter initially, where you get shot at by the + ballista. + +* **Side lake:** The small lake accessible via a passage from the larger one, in + which you face Horace the Hushed as part of his quest. + +* **Ruins main:** The area you first enter after the Demon Ruins bonfire. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the stairs. + +* **Antechamber:** The area up the flight of stairs near the +Old King's Antechamber bonfire. + +* **Ruins basement:** The area further down from ruins main lower, with many + basilisks and Knight Slayer Tsorig in vanilla. + +### Irithyll of the Boreal Valley + +This region starts _after_ the Small Doll wall and ends with Pontiff Sulyvahn. +Everything after that, including the contents of Sulyvahn's cathedral is +considered part of Anor Londo. + +* **Central:** The beginning of the region, from the Central Irithyll bonfire up + to the plaza. + +* **Dorhys:** The sobbing mob (a Cathedral Evangelist in vanilla) behind the + locked door opening onto central. Accessed through an illusory railing by the + crystal lizard just before the plaza. + +* **Plaza:** The area in front of and below the cathedral, with a locked door up + to the cathedral and a locked elevator to the Ascent. + +* **Descent:** The path from the Church of Yorshka bonfire down to the lake. + +* **Lake:** The open watery area outside the room with the Distant Manor + bonfire. + +* **Sewer:** The room between the lake and the beginning of the ascent, filled + with Sewer Centipedes in vanilla. + +* **Ascent:** The path up from the lake to the cathedral, through several + buildings and some open stairs. + +* **Great hall:** The building along the ascent with a large picture of + Gwynevere and several Silver Knights in vanilla. + +### Irithyll Dungeon + +In Irithyll Dungeon locations, "left" and "right" are always oriented as though +"near" is where you stand and "far" is where you're facing. (For example, you +enter the dungeon from the bonfire on the near left.) + +* **B1:** The floor on which the player enters the dungeon, with the Irithyll + Dungeon bonfire. + + * "Near" is the side of the dungeon with the bonfire. + * "Far" is the opposite side. + +* **B2:** The floor directly below B1, which can be reached by going down the + stairs or dropping. + + * "Near" is the same side of the dungeon as the bonfire. + * "Far" is the opposite side. + +* **Pit:** The large room with the Giant Slave and many Rats in vanilla. + +* **Pit lift:** The elevator from the pit up to B1 near, right to the Irithyll + Dungeon bonfire. + +* **B3:** The lowest floor, with Karla's cell, a lift back to B2, and the exit + onwards to the Profaned Capital. + + * "Near" is the side with Karla's cell and the path from the pit. + * "Far" is the opposite side with the mimic. + +* **B3 lift:** The elevator from B3 (near where you can use Path of the Dragon + to go to Archdragon Peak) up to B2. + +### Profaned Capital + +* **Tower:** The tower that contains the Profaned Capital bonfire. + +* **Swamp:** The pool of toxic liquid accessible by falling down out of the + lower floor of the tower, going into the corridor to the left, and falling + down a hole. + +* **Chapel:** The building in the swamp containing Monstrosities of Sin in + vanilla. + +* **Bridge:** The long bridge from the tower into the palace. + +* **Palace:** The large building carved into the wall of the cavern, full of + chalices and broken pillars. + +### Anor Londo + +This region includes everything after Sulyvahn's cathedral, including its upper +story. + +* **Light cathedral:** The cathedral in which you fight Pontiff Sulyvahn in + vanilla. + +* **Plaza:** The wide open area filled with Giant Slaves in vanilla. + +* **Walkway:** The path above the plaza leading to the second floor of the light + cathedral, with Deacons in vanilla. + +* **Buttresses:** The flying buttresses that you have to climb to get to the + spiral staircase. "Near" and "far" are relative to the light cathedral, so the + nearest buttress is the one that leads back to the walkway. + +* **Tomb:** The area past the illusory wall just before the spiral staircase, in + which you marry Anri during Yoel and Yuria's quest. + +* **Dark cathedral:** The darkened cathedral just before the Aldrich fight in + vanilla. + +### Lothric Castle + +This region covers everything up the ladder from the Dancer of the Boreal Valley +bonfire up to the door into Grand Archives, except the area to the left of the +ladder which is part of Consumed King's Garden. + +* **Lift:** The elevator from the room straight after the Dancer of the Boreal + Valley bonfire up to just before the boss fight. + +* **Ascent:** The set of stairways and turrets leading from the Lothric Castle + bonfire to the Dragon Barracks bonfire. + +* **Barracks:** The large building with two fire-breathing wyverns across from + the Dragon Barracks bonfire. + +* **Moat:** The ditch beneath the bridge leading to the barracks. + + * The "right path" leads to the right as you face the barracks, around and + above the stairs up to the Dragon Barracks bonfire. + +* **Plaza:** The open area in the center of the barracks, where the two wyverns + breathe fire. + + * "Left" is the enclosed area on the left as you're coming from the Dragon + Barracks bonfire, with the stairs down to the basement. + +* **Basement:** The room beneath plaza left, with the Boreal Outrider in + vanilla. + +* **Dark room:** The large darkened room on the right of the barracks as you're + coming from the Dragon Barracks bonfire, with firebomb-throwing Hollows in + vanilla. + + * "Lower" is the bottom floor that you enter onto from the plaza. + * "Upper" is the top floor with the door to the main hall. + * "Mid" is the middle floor accessible by climbing a ladder from lower or + going down stairs from upper. + +* **Main hall:** The central room of the barracks, behind the gate. + +* **Chapel:** The building to the right just before the stairs to the boss, with + a locked elevator to Grand Archives. + +* **Wyvern room:** The room where you can fight the Pus of Man infecting the + left wyvern, accessible by dropping down to the left of the stairs to the + boss. + +* **Altar:** The building containing the Altar of Sunlight, accessible by + climbing up a ladder onto a roof around the corner from the stairs to the + boss. + +### Consumed King's Garden + +This region covers everything to the left of the ladder up from the Dancer of +the Boreal Valley bonfire up to the illusory wall into Untended Graves. + +* **Balcony:** The walkway accessible by getting off the first elevator halfway + down. + +* **Rotunda:** The building in the center of the toxic pool, with a Cathedral + Knight on it in vanilla. + +* **Lone stairway:** A set of stairs leading nowhere in the far left of the main + area as you enter from the first elevator. + +* **Shortcut:** The path from the locked door into Lothric Castle, through the + room filled with thralls in vanilla, and down a lift. + +* **Tomb:** The area after the boss room. + +### Grand Archives + +* **1F:** The first floor of the Grand Archives, including the first wax pool. + +* **Dark room:** The unlit room on 1F to the right of the wax pool. + +* **2F:** The second floor of the grand archives. It's split into two sections + that are separated by retractable bookshelves. + + * "Early" is the first part you reach and has an outdoor balcony with a ladder + to 3F and a wax pool up a short set of stairs. + * "Late" is the part you can only reach by climbing down from F3, where you + encounter the teleporting miniboss for the final time. + +* **3F:** The third floor of the grand archives, where you encounter the + teleporting miniboss for the second time. Includes the area with a hidden room + with another miniboss. + +* **4F:** The topmost and most well-lit section of bookshelves, overlooking the + rest of the archives. + +* **Rooftops:** The outer rooftop area between 4F and 5F, with Gargoyles in + vanilla. + + * "Lower" is the balcony you can reach by dropping off the rooftops, as well + as the further rooftops leading down to the 2F early balcony. + +* **5F:** The topmost floor of the archives interior, accessible from the + rooftops, with a ladder down to 4F. + +* **Dome:** The domed roof of the Grand Archives, with Ascended Winged Knights + in vanilla. + +* **Rafters:** The narrow walkways above the Grand Archives, accessible by + dropping down from the dome. + +### Untended Graves + +* **Swamp:** The watery area immediately after the Untended graves bonfire, up + to the cemetery. + +* **Cemetery:** The area past where the Cemetery of Ash bonfire would be, up to + the boss arena. + +* **Environs:** The area after the boss and outside the abandoned Firelink + Shrine. + +* **Shrine:** The area inside the abandoned Firelink Shrine. + +### Archdragon Peak + +"Gesture" always means the Path of the Dragon gesture. + +* **Intro:** The first section, from where you warp in from Irithyll Dungeon up + to the first boss fight. + + * "Archway": The large stone archway in front of the boss door. + +* **Fort:** The arena where you fight Ancient Wyvern in vanilla. + + * "Overlook": The area down the stairs from where the Ancient Wyvern first + lands in vanilla, overlooking the fog. + + * "Rotunda": The top of the spiral staircase building, to the left before the + bridge with the chain-axe Man-Serpent in vanilla. + +* **Mausoleum:** The building with the Dragon-Kin Mausoleum bonfire, where + you're warped after the first boss fight. + +* **Walkway:** The path from the mausoleum to the belfry, looking out over + clouds. + + * "Building": The building along the walkway, just before the wyvern in + vanilla. + +* **Belfry:** The building with the Great Belfry bonfire, including the room + with the summoner. + +* **Plaza:** The arena that appears after you defeat Nameless King in vanilla. + +* **Summit:** The path up from the belfry to the final altar at the top of the + mountain. + +### Painted World of Ariandel (Before Contraption) + +This region covers the Ashes of Ariandel DLC up to the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter. + +* **Snowfield:** The area around the Snowfield bonfire, + + * "Upper": The area immediately after the Snowfield bonfire, before the + collapsing overhang, with the Followers in vanilla. + + * "Lower": The snowy tree-filled area after the collapsing overhang, with the + Wolves in vanilla. + + * "Village": The area with broken-down buildings and Millwood Knights in + vanilla. + + * "Tower": The tower by the village, with Millwood Knights in Vanilla. + +* **Bridge:** The rope bridge to the chapel. + + * "Near": The side of the bridge by the Rope Bridge Cave bonfire. + + * "Far": The side of the bridge by the Ariandel Chapel bonfire. + +* **Chapel:** The building with the Ariandel Chapel bonfire and Lady Friede. + +* **Depths:** The area reachable by cutting down the bridge and descending on + the far side, with the Depths of the Painting bonfire. + +* **Settlement:** The area reachable by cutting down the bridge and descending + on the near side, with the Corvian Settlement bonfire. Everything after the + slide down the hill is considered part of the settlement. + + * "Courtyard": The area in front of the settlement, immediately after the + slide. + + * "Main": The main road of the settlement leading up to the locked gate to the + library. Also includes the buildings that are immediately accessible from + this road. + + * "Loop": A side path that loops left from the main road and goes up and + behind the building with the bonfire. + + * "Back": The back alley of the settlement, accessible by dropping down to the + right of the locked gate to the library. Also includes the buildings that + are immediately accessible from this alley. + + * "Roofs": The village rooftops, first accessible by climbing a ladder from + the back alley. Also includes the buildings and items that are first + accessible from the roofs. + + * "Hall": The largest building in the settlement, with two Corvian Knights in + vanilla. + +* **Library:** The building where you use the contraption key, where Vilhelm + appears in vanilla. + +### Painted World of Ariandel (After Contraption) + +This region covers the Ashes of Ariandel DLC past the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter, including the basement beneath the chapel. + +* **Pass:** The mountainous area past the Snowy Mountain Pass bonfire. + +* **Pit:** The area with a large tree and numerous Millwood Knights in vanilla, + reached by a collapsing overhang in the pass. + +* **B1:** The floor immediately below the chapel, first accessible from the + pass. Filled with Giant Flies in vanilla. + +* **B2:** The floor below B1, with lots of fly eggs. Filled with even more Giant + Flies than B1 in vanilla. + +* **B3:** The floor below B2, accessible through an illusory wall. + +* **Rotunda:** The round arena out in the open, accessible by platforming down + tree roots from B3. + +### Dreg Heap + +* **Shop:** Items sold by the Stone-Humped Hag by The Dreg Heap bonfire. + +* **Castle:** The building with The Dreg Heap bonfire, up to the large fall into + the library. + +* **Library:** The building with the stained-glass window that you fall into + from the castle. + +* **Church:** The building below and to the right of the library, which the + pillar falls into to make a bridge. + +* **Pantry:** The set of rooms entered through a door near the fountain just + past the church, with boxes and barrels. + + * "Upstairs": The room with an open side, accessible through an illusory wall + in the furthest pantry room. + +* **Parapets:** The area with balconies and Overgrown Lothric Knights in + vanilla, accessible by taking the pillar bridge from the church, following + that path to the end, and dropping down to the right. + +* **Ruins:** The area around the Earthen Peak Ruins bonfire, up to the swamp. + +* **Swamp:** The area in and above the poisonous water, up to the point the + branches deposit you back on the ruins. + + * "Left": Left as you enter from the ruins, towards the cliff edge. + + * "Right": Right as you enter from the ruins, towards higher ground. + + * "Upper": The path up and over the swamp towards the Within Earthen Peak + Ruins bonfire. + +### Ringed City + +The "mid boss", "end boss", and "hidden boss" are the bosses who take the place +of Halflight, Gael, and Midir, respectively. + +* **Wall:** The large wall in which you spawn when you first enter the area, + with the Mausoleum Lookout bonfire. + + * "Top": The open-air top of the wall, where you first spawn in. + + * "Upper": The upper area of the wall, with the Ringed Inner Wall bonfire. + + * "Tower": The tiered tower leading down from the upper area to the stairs. + + * "Lower": The lower rooms of the wall, accessible from the lower cliff, with + an elevator back to upper. + + * "Hidden": The hidden floor accessible from the elevator from lower to upper, + from which you can reach Midir in vanilla. + +* **Streets:** The streets and skyways of the city proper. "Left" and "right" + are relative to the main staircase as you head down towards the swamp, "near" + and "far" are relative to Shira's chamber at the top of the stairs. + + * "Garden": The flower-filled back alley accessible from the left side of the + nearest bridge over the stairs. + + * "High": The higher areas in the far left where you can find the Locust + Preacher, accessible from a long ladder in the swamp. + + * "Monument": The area around the purging monument, which can only be accessed + by solving the "Show Your Humanity" puzzle. + +* **Swamp:** The wet area past the city streets. "Left" and "right" are relative + to heading out from the Ringed City Streets bonfire, and "near" and "far" are + relative to that bonfire as well. + +* **Lower cliff:** The cliffside path leading from the swamp into the shared + grave, where Midir breathes fire. + +* **Grave:** The cylindrical chamber with spiral stairs around the edges, + connecting the two cliffs, containing the Shared Grave bonfire. + +* **Upper cliff:** The cliffside path leading out of the grave to the lower + wall. + +* **Church path:** The sunlit path from the lower cliff up to the Church of + Filianore where you fight Halflight in vanilla. + +* **Ashes:** The final area, where you fight Gael in vanilla. + +## Detailed Location Descriptions + +These location descriptions were originally written by [Matt Gruen] for [the +static _Dark Souls III_ randomizer]. + +[Matt Gruen]: https://thefifthmatt.com/ +[the static _Dark Souls III_ randomizer]: https://www.nexusmods.com/darksouls3/mods/361 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location nameDetailed description
AL: Aldrich Faithful - water reserves, talk to McDonnelGiven by Archdeacon McDonnel in Water Reserves.
AL: Aldrich's Ruby - dark cathedral, minibossDropped by the Deep Accursed who drops down when you open the Anor Londo Cathedral shortcut
AL: Anri's Straight Sword - Anri questDropped by Anri of Astora upon death or completing quest. In the Darkmoon Tomb with Lord of Hollows route, or given by Ludleth if summoned to defeat Aldrich.
AL: Blade of the Darkmoon - Yorshka with Darkmoon LoyaltyGiven by Yorshka after learning the Darkmoon Loyalty gesture from Sirris, or by killing her
AL: Brass Armor - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Gauntlets - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Helm - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Leggings - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Chameleon - tomb after marrying AnriDropped by the Stone-humped Hag assassin after Anri reaches the Church of Yorshka, either in the church or after marrying Anri
AL: Cinders of a Lord - AldrichDropped by Aldrich
AL: Crescent Moon Sword - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Dark Stoneplate Ring - by dark stairs up from plazaAfter the Pontiff fight, in the dark hallways to the left of the area with the Giant Slaves
AL: Deep Gem - water reservesIn the open in the Water Reserves
AL: Dragonslayer Greatarrow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Dragonslayer Greatbow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Drang Twinspears - plaza, NPC dropDropped by Drang Twinspears-wielding knight on the stairs leading up to the Anor Londo Silver Knights
AL: Easterner's Ashes - below top of furthest buttressDropping down from the rightmost flying buttress, or the rightmost set of stairs
AL: Ember - plaza, furtherAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Ember - plaza, right sideAfter the Pontiff fight, next to one of the Giant Slaves on the right side
AL: Ember - spiral staircase, bottomNext to the lever that summons the rotating Anor Londo stairs at the bottom
AL: Estus Shard - dark cathedral, by left stairsIn a chest on the floor of the Anor Londo cathedral
AL: Giant's Coal - by giant near dark cathedralOn the Giant Blacksmith's corpse in Anor Londo
AL: Golden Ritual Spear - light cathedral, mimic upstairsDrop from a mimic in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Havel's Ring+2 - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Human Dregs - water reservesIn the open in the Water Reserves
AL: Large Soul of a Weary Warrior - left of dark cathedral entranceIn front of the Anor Londo cathedral, slightly to the left
AL: Large Titanite Shard - balcony by dead giantsAfter the Pontiff fight, on the balcony to the right of the area with the Giant Slaves
AL: Large Titanite Shard - bottom of the furthest buttressAt the base of the rightmost flying buttress leading up to Anor Londo
AL: Large Titanite Shard - bottom of the nearest buttressOn the tower leading back from Anor Londo to the shortcut to Irithyll, down the flying buttress closest to the Darkmoon Tomb entrance.
AL: Large Titanite Shard - right after light cathedralAfter Pontiff's cathedral, hugging the wall to the right
AL: Large Titanite Shard - walkway, side path by cathedralAfter the Pontiff fight, going back from the Deacons area to the original cathedral, before a dropdown
AL: Moonlight Arrow - dark cathedral, up right stairsIn the Anor Londo cathedral, up the stairs on the right side
AL: Painting Guardian Gloves - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Gown - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Hood - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Waistcloth - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian's Curved Sword - prison tower raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Proof of a Concord Kept - dark cathedral, up left stairsIn the Anor Londo cathedral, halfway down the stairs on the left side next to some Deacons
AL: Reversal Ring - tomb, chest in cornerIn a chest in Darkmoon Tomb
AL: Ring of Favor - water reserves, both minibossesDropped after killing both of Sulyvahn's Beasts in the Water Reserves
AL: Ring of Favor+1 - light cathedral, upstairsIn the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Silver Mask - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Simple Gem - light cathedral, lizard upstairsDropped by a Crystal Lizard in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Soul of AldrichDropped by Aldrich
AL: Soul of Rosaria - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Soul of a Crestfallen Knight - right of dark cathedral entranceTo the right of the Anor Londo cathedral entrance, past the red-eyed Silver Knight
AL: Soul of a Weary Warrior - plaza, nearerAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Sun Princess Ring - dark cathedral, after bossIn the Anor Londo cathedral after defeating Aldrich, up the elevators in Gwynevere's Chamber
AL: Titanite Scale - top of ladder up to buttressesOn the platform after the stairs leading up to Anor Londo from the Water Reserves building
AL: Twinkling Titanite - lizard after light cathedral #1Dropped a Crystal Lizard straight after the Pontiff fight
AL: Twinkling Titanite - lizard after light cathedral #2Dropped a Crystal Lizard straight after the Pontiff fight
AL: Yorshka's Chime - kill YorshkaDropped by Yorshka upon death.
AP: Ancient Dragon Greatshield - intro, on archwayAfter the Archdragon Peak bonfire, on top of the arch in front of the Ancient Wyvern fight
AP: Calamity Ring - mausoleum, gesture at altarReceived using Path of the Dragon at the Altar by the Mausoleum bonfire
AP: Covetous Gold Serpent Ring+2 - plazaIn the Nameless King boss arena after he is defeated
AP: Dragon Chaser's Ashes - summit, side pathIn the run-up to the Dragon Altar after the Belfry bonfire, in a side path to the left side
AP: Dragon Head Stone - fort, boss dropDropped by Ancient Wyvern
AP: Dragon Tooth - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Dragonslayer Armor - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Gauntlets - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Helm - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Leggings - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Spear - gate after mausoleumIn the gate connecting the Dragon-Kin Mausoleum area to the bridge where the Nameless King fight takes place
AP: Drakeblood Greatsword - mausoleum, NPC dropDropped by the Drakeblood Knight summoned by the Serpent-Man Summoner
AP: Dung Pie - fort, landing after second roomOn a landing going up the stairs from the Ancient Wyvern to the chainaxe Man-Serpent area
AP: Ember - belfry, below bellIn the area below the bell lever, either dropping down near the lever or going down the stairs from the open fountain area after the Belfry bonfire
AP: Ember - fort overlook #1From the right of where Ancient Wyvern first lands
AP: Ember - fort overlook #2From the right of where Ancient Wyvern first lands
AP: Ember - intro, by bonfireNext to the Archdragon Peak bonfire
AP: Great Magic Barrier - drop off belfry roofDropping down to the left from the area with the Havel Knight and the dead Wyvern
AP: Havel's Greatshield - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Havel's Ring+1 - summit, after buildingJust past the building with all of the Man-Serpents on the way to the Dragon Altar, on the left side
AP: Homeward Bone - intro, path to bonfireFrom the start of the area, along the left path leading to the first bonfire
AP: Large Soul of a Crestfallen Knight - summit, by fountainIn the middle of the open fountain area after the Belfry bonfire
AP: Large Soul of a Nameless Soldier - fort, by stairs to first roomto the left of where the Ancient Wyvern lands
AP: Large Soul of a Weary Warrior - fort, centerWhere the Ancient Wyvern lands
AP: Lightning Bolt - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Lightning Clutch Ring - intro, left of boss doorTo the left of gate leading to Ancient Wyvern, past the Rock Lizard
AP: Lightning Gem - intro, side riseFrom the start of the area, up a ledge in between two forked paths toward the first bonfire
AP: Lightning Urn - fort, left of first room entranceOn the path to the left of where the Ancient Wyvern lands, left of the building entrance
AP: Ricard's Rapier - belfry, NPC dropDropped by the Richard Champion summoned by the Serpent-Man Summoner
AP: Ring of Steel Protection - fort overlook, beside stairsTo the right of the area where the Ancient Wyvern lands, dropping down onto the ledge
AP: Soul of a Crestfallen Knight - mausoleum, upstairsFrom the Mausoleum bonfire, up the second set of stairs to the right
AP: Soul of a Nameless Soldier - intro, right before archwayFrom the Archdragon Peak bonfire, going right before the arch before Ancient Wyvern
AP: Soul of a Weary Warrior - intro, first cliff edgeAt the very start of the area on the left cliff edge
AP: Soul of a Weary Warrior - walkway, building windowOn the way to the Belfry bonfire after the sagging wooden bridge, on a ledge visible in a room with a Crystal Lizard, accessible by a tricky jump or just going around the other side
AP: Soul of the Nameless KingDropped by Nameless King
AP: Stalk Dung Pie - fort overlookFrom the right of where Ancient Wyvern first lands
AP: Thunder Stoneplate Ring - walkway, up ladderAfter the long hallway after the Mausoleum bonfire, before the rope bridge, up the long ladder
AP: Titanite Chunk - fort, second room balconyAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left
AP: Titanite Chunk - intro, archway cornerFrom the Archdragon Peak bonfire, under the arch, immediately to the right
AP: Titanite Chunk - intro, behind rockAlmost at the Archdragon Peak bonfire, behind a rock in the area with many Man-Serpents
AP: Titanite Chunk - intro, left before archwayAfter the Archdragon Peak bonfire, going left before the arch before Ancient Wyvern
AP: Titanite Chunk - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Titanite Chunk - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Scale - mausoleum, downstairs balcony #1From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, downstairs balcony #2From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, upstairs balconyFrom the Mausoleum bonfire, up the first stairs to the right, going around toward the Man-Serpent Summoner, on the balcony on the side
AP: Titanite Scale - walkway buildingIn a chest after the sagging wooden bridge on the way to the Belfry, in the building with the Crystal Lizard
AP: Titanite Scale - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Slab - belfry roofNext to the Havel Knight by the dead Wyvern
AP: Titanite Slab - plazaIn the Nameless King boss arena after he is defeated
AP: Twinkling Dragon Torso Stone - summit, gesture at altarReceived using Path of the Dragon at the Altar after the Belfry bonfire. Hawkwood also uses the gesture there when summoned.
AP: Twinkling Titanite - belfry, by ladder to roofIn the chest before the ladder climbing up to the Havel Knight
AP: Twinkling Titanite - fort, down second room balcony ladderAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left and then down the ladder
AP: Twinkling Titanite - fort, end of raftersDropping down to the left of the Mausoleum bonfire, all the way down the wooden rafters
AP: Twinkling Titanite - walkway building, lizardDropped by Crystal Lizard in the building after the sagging wooden bridge toward the Belfry
AP: Twinkling Titanite - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
CA: Coiled Sword - boss dropDropped by Iudex Gundyr
CA: Firebomb - down the cliff edgeAlong the cliff edge before the Iudex Gundyr fight, to the right
CA: Soul of a Deserted Corpse - right of spawnAt the very start of the game
CA: Soul of an Unknown Traveler - by minibossIn the area with the Ravenous Crystal Lizard
CA: Speckled Stoneplate Ring+1 - by minibossIn the area with the Ravenous Crystal Lizard, along the right wall
CA: Titanite Scale - miniboss dropDropped by Ravenous Crystal Lizard
CA: Titanite Shard - jump to coffinMaking a jump to a coffin after the Cemetery of Ash bonfire
CC: Black Blade - tomb, mimicDropped by the mimic before Smouldering Lake
CC: Black Bug Pellet - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Bloodred Moss Clump - atrium lower, down more stairsTo the left before going down the main stairwell in the Catacombs, past the skeleton ambush and where Anri is standing, near the Crystal Lizard
CC: Carthus Bloodring - crypt lower, end of side hallAt the very end of the Bonewheel Skeleton area
CC: Carthus Milkring - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots
CC: Carthus Pyromancy Tome - atrium lower, jump from bridgeDown the hallway to the right before going down the main stairwell in the Catacombs and through an illusory wall on the left, or making a difficult dropdown from the top-level platform
CC: Carthus Rouge - atrium upper, left after entranceTo the right after first entering the Catacombs
CC: Carthus Rouge - crypt across, cornerMaking a difficult jump between the hallway after the first Skeleton Ball and the area at the same level on the opposite side, or going up the stairs from the main hall
CC: Dark Gem - crypt lower, skeleton ball dropDropped by second Skeleton Ball after killing its sorcerer skeleton
CC: Ember - atrium, on long stairwayOn the main stairwell in Catacombs
CC: Ember - crypt lower, shortcut to cavernIn the short hallway with the level shortcut where Knight Slayer Tsorig invades
CC: Ember - crypt upper, end of hall past holeGoing right from the Catacombs bonfire, down the hall to the left, then to the right. After a hole that drops down into the Bonewheel Skeleton area.
CC: Fire Gem - cavern, lizardDropped by a Crystal Lizard found between the Catacombs main halls and the ledge overlooking the bridge you can cut down
CC: Grave Warden Pyromancy Tome - boss arenaIn Wolnir's arena, or in the back left of the room containing his bonfire if not picked up in the arena
CC: Grave Warden's Ashes - crypt across, cornerFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, on the far left side. Stairwell past the illusory wall is most direct.
CC: Homeward Bone - Irithyll bridgeFound right before the wall blocking access to Irithyll
CC: Large Soul of a Nameless Soldier - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Large Soul of a Nameless Soldier - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Large Soul of an Unknown Traveler - crypt upper, hall middleGoing right from the Catacombs bonfire, then down the long hallway after the hallway to the left
CC: Large Titanite Shard - crypt across, middle hallFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, in a middle hallway
CC: Large Titanite Shard - crypt upper, skeleton ball hallGoing right from the Catacombs bonfire, to the end of the hallway where second Skeleton Ball rolls
CC: Large Titanite Shard - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Old Sage's Blindfold - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Pontiff's Right Eye - Irithyll bridge, miniboss dropDropped by killing Sulyvahn's Beast on the bridge to Irithyll or in the lake below
CC: Ring of Steel Protection+2 - atrium upper, drop onto pillarFrom the first bridge in Catacombs where the first skeletons are encountered, parallel to the long stairwell, walk off onto a pillar on the left side.
CC: Sharp Gem - atrium lower, right before exitDown the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of High Lord WolnirDropped by High Lord Wolnir
CC: Soul of a Demon - tomb, miniboss dropDropped by the Fire Demon before Smouldering Lake
CC: Soul of a Nameless Soldier - atrium lower, down hallAll the way down the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of a Nameless Soldier - atrium upper, up more stairsFrom the room before the Catacombs main stairwell, up the two ramps and to the end of the long hallway crossing the room
CC: Thunder Stoneplate Ring+1 - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots, behind one of the pillars
CC: Titanite Shard - atrium lower, corner by stairsTo the left before going down the main stairwell in the Catacombs, behind the pensive Carthus Cursed Sword Skeleton
CC: Titanite Shard - crypt lower, left of entranceIn the main hall after the Catacombs bonfire, down the stairs and to the left
CC: Titanite Shard - crypt lower, start of side hallIn the Bonewheel Skeleton area, on the left side under a Writhing Flesh
CC: Twinkling Titanite - atrium lower, lizard down more stairsDropped by a Crystal Lizard found to the left before going down the main stairwell in the Catacombs, past the skeleton ambush and past where Anri is standing
CC: Undead Bone Shard - crypt upper, skeleton ball dropDropped by first Skeleton Ball after killing its sorcerer skeleton
CC: Witch's Ring - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Yellow Bug Pellet - cavern, on overlookTo the right of the Carthus Curved Sword Skeleton overlooking the pit Horace falls into
CD: Aldrich's Sapphire - side chapel, miniboss dropDropped by the Deep Accursed
CD: Arbalest - upper roofs, end of furthest buttressBefore the rafters on the way to Rosaria, up a flying buttress, past a halberd-wielding Large Hollow Soldier to the right, and down another flying buttress to the right
CD: Archdeacon Holy Garb - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon Skirt - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon White Crown - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Armor of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Astora Greatsword - graveyard, left of entranceDown one of the side paths to the left in the Reanimated Corpse area
CD: Barbed Straight Sword - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Black Eye Orb - Rosaria from Leonhard's questOn Rosaria's corpse after joining Rosaria's Fingers, exhausting Leonhard's dialogue there and reaching the Profaned Capital bonfire.
CD: Blessed Gem - upper roofs, raftersIn the rafters leading to Rosaria, guarded by a Cathedral Knight to the right
CD: Crest Shield - path, drop down by Cathedral of the Deep bonfireOn a grave near the Cathedral of the Deep bonfire, accessed by dropping down to the right
CD: Curse Ward Greatshield - by ladder from white tree to moatTaking a right after the Infested Corpse graveyard, before the shortcut ladder down to the Ravenous Crystal Lizard area
CD: Deep Braille Divine Tome - mimic by side chapelDropped by the Mimic before the room with the patrolling Cathedral Knight and Deep Accursed
CD: Deep Gem - down stairs by first elevatorComing from the room where you first see deacons, go down instead of continuing to the main cathedral room. Guarded by a pensive Cathedral Evangelist.
CD: Deep Ring - upper roofs, passive mob drop in first towerDropped by the passive Deacon on the way to Rosaria
CD: Drang Armor - main hall, eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Gauntlets - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Hammers - main hall eastIn the Giant Slave muck pit leading up to Deacons, underneath the stairwell
CD: Drang Shoes - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Duel Charm - by first elevatorAfter opening the cathedral's backdoor, where the Deacon enemies are first seen, under a fountain that spouts poison
CD: Duel Charm - next to Patches in onion armorTo the right of the bridge leading to Rosaria, from the Deacons side. Patches will lower the bridge if you try to cross from this side.
CD: Ember - PatchesSold by Patches in Firelink Shrine
CD: Ember - by back doorPast the pair of Grave Wardens and the Cathedral backdoor against a wall, guarded by a greataxe-wielding Large Hollow Soldier
CD: Ember - edge of platform before bossOn the edge of the chapel before Deacons overlooking the Giant Slaves
CD: Ember - side chapel upstairs, up ladderUp a ladder and past the Cathedral Evangelist from the top level of the room with the patrolling Cathedral Knight and Deep Accursed
CD: Ember - side chapel, miniboss roomIn the room with the Deep Accursed
CD: Estus Shard - monument outside Cleansing ChapelRight outside of the Cleansing Chapel. Requires killing praying hollows.
CD: Executioner's Greatsword - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area
CD: Exploding Bolt - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Fading Soul - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area, next to the Executioner's Greatsword
CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Helm of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Herald Armor - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Gloves - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Helm - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Trousers - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Homeward Bone - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Horsehoof Ring - PatchesSold or dropped by Patches after he mentions Greirat
CD: Large Soul of an Unknown Traveler - by white tree #1In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - by white tree #2In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balconyOn the cathedral roof after climbing up the flying buttresses, on the edge of the semicircle platform balcony
CD: Large Soul of an Unknown Traveler - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Large Soul of an Unknown Traveler - main hall south, side pathDown a side path with poison-spouting fountains in the main cathedral room, accessible from the Cleansing Chapel shortcut, patrolled by a Cathedral Knight
CD: Large Soul of an Unknown Traveler - path, against outer wallFrom the Cathedral of the Deep bonfire after the Brigand, against the wall in the area with the dogs and crossbowmen
CD: Leggings of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Lloyd's Sword Ring - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Maiden Gloves - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Hood - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Robe - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Skirt - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Notched Whip - Cleansing ChapelIn a corner of the Cleansing Chapel
CD: Paladin's Ashes - path, guarded by lower NPCAt the very start of the area, guarded by the Fallen Knight
CD: Pale Tongue - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Pale Tongue - upper roofs, outdoors far endBefore the rafters on the way to Rosaria, up a flying buttress and straight right, passing a halberd-wielding Large Hollow Soldier
CD: Poisonbite Ring - moat, hall past minibossIn the pit with the Infested Corpse, accessible from the Ravenous Crystal Lizard area or from dropping down near the second Cleansing Chapel shortcut
CD: Red Bug Pellet - lower roofs, up stairs between buttressesIn the area after the cathedral roof against the wall of the cathedral, down the path from the Cathedral Evangelist.
CD: Red Bug Pellet - right of cathedral front doorsUp the stairs past the Infested Corpse graveyard and the left, toward the roof path to the right of the cathedral doors
CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed ChamberDropped by passive Corpse-grub against the wall near the entrance to Rosaria's Bed Chamber
CD: Repair Powder - by white treeIn the graveyard with the White Birch and Infested Corpses
CD: Ring of Favor+2 - upper roofs, on buttressBefore the rafters on the way to Rosaria, up a flying buttress, behind a greataxe-wielding Large Hollow Soldier to the left
CD: Ring of the Evil Eye+1 - by stairs to bossBefore the stairs leading down into the Deacons fight
CD: Rosaria's Fingers - RosariaGiven by Rosaria.
CD: Rusted Coin - don't forgive PatchesGiven by Patches after not forgiving him after he lowers the bridge in Cathedral of the Deep.
CD: Rusted Coin - left of cathedral front doors, behind cratesUp the stairs past the Infested Corpse graveyard and to the left, hidden behind some crates to the left of the cathedral door
CD: Saint Bident - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Saint-tree Bellvine - moat, by waterIn the Infested Corpse moat beneath the Cathedral
CD: Seek Guidance - side chapel upstairsAbove the room with the patrolling Cathedral Knight and Deep Accursed, below a writhing flesh on the ceiling.
CD: Shotel - PatchesSold by Patches
CD: Small Doll - boss dropDropped by Deacons of the Deep
CD: Soul of a Nameless Soldier - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Soul of a Nameless Soldier - lower roofs, side roomComing from the cathedral roof, past the three crossbowmen to the path patrolled by the halberd-wielding Large Hollow Soldier, in a room to the left with many thralls.
CD: Soul of a Nameless Soldier - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Soul of the Deacons of the DeepDropped by Deacons of the Deep
CD: Spider Shield - NPC drop on pathDropped by the brigand at the start of Cathedral of the Deep
CD: Spiked Shield - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Titanite Scale - moat, miniboss dropDropped by the Ravenous Crystal Lizard outside of the Cathedral
CD: Titanite Shard - Cleansing Chapel windowsill, by minibossOn the ledge dropping back down into Cleansing Chapel from the area with the Ravenous Crystal Lizard
CD: Titanite Shard - moat, far endBehind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Titanite Shard - moat, up a slopeUp one of the slopes in the Ravenous Crystal Lizard area
CD: Titanite Shard - outside building by white treePast the Infested Corpse graveyard to the left, hidden along the left wall of the building with the shortcut ladder and Curse Ward Greatshield
CD: Titanite Shard - path, side path by Cathedral of the Deep bonfireUp a path to the left after the Cathedral of the Deep bonfire, after the Fallen Knight and before the Brigand
CD: Twinkling Titanite - moat, lizard #1Dropped by the Crystal Lizard behind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - moat, lizard #2Dropped by the Crystal Lizard under the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - path, lizard #1Dropped by the first Crystal Lizard after the Crystal Sage fight
CD: Twinkling Titanite - path, lizard #2Dropped by the second Crystal Lizard after the Crystal Sage fight
CD: Undead Bone Shard - gravestone by white treeIn the graveyard with the Infested Corpses, on a coffin partly hanging off of the ledge
CD: Undead Hunter Charm - lower roofs, up stairs between buttressesIn the area after the cathedral roof guarded by a Cathedral Evangelist. Can be jumped to from a flying buttress or by going around and back
CD: Winged Spear - kill PatchesDropped by Patches when killed in his own armor.
CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Young White Branch - by white tree #1By the White Birch tree in the Infested Corpse graveyard
CD: Young White Branch - by white tree #2By the White Birch tree in the Infested Corpse graveyard
CKG: Black Firebomb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Claw - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Dark Gem - under lone stairwayFollowing the left wall, behind the standalone set of stairs
CKG: Dragonscale Ring - shortcut, leave halfway down liftFrom the middle level of the second elevator, toward the Oceiros boss fight
CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Estus Shard - balconyFrom the middle level of the first Consumed King's Gardens elevator, out the balcony and to the right
CKG: Human Pine Resin - by lone stairway bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool
CKG: Human Pine Resin - toxic pool, past rotundaIn between two platforms near the middle of the garden, by a tree in a toxic pool
CKG: Magic Stoneplate Ring - mob drop before bossDropped by the Cathedral Knight closest to the Oceiros fog gate
CKG: Ring of Sacrifice - under balconyAlong the right wall of the garden, next to the first elevator building
CKG: Sage Ring+2 - balcony, drop onto rubble, jump backFrom the middle platform of the first elevator in the target, going out and dropping off to the left, and then running off onto the ruined arch behind.
CKG: Shadow Garb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Gauntlets - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Leggings - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Mask - under center platformUnder the platform in the middle of the garden, in the toxic pool
CKG: Soul of Consumed OceirosDropped by Consumed King Oceiros
CKG: Soul of a Weary Warrior - before first liftOn the path leading to the first elevator from Lothric Castle
CKG: Titanite Chunk - balcony, drop onto rubbleFrom the middle platform of the first elevator, dropping down to the left
CKG: Titanite Chunk - right of shortcut lift bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, all the way to the end
CKG: Titanite Chunk - shortcutRight inside of the shortcut door leading to Oceiros from Lothric/Dancer bonfire
CKG: Titanite Chunk - up lone stairwayFollowing the left wall of the garden, in and up the standalone set of stairs
CKG: Titanite Scale - shortcutIn the room leading to the Oceiros shortcut elevator from Lothric/Dancer, in the first floor alcove.
CKG: Titanite Scale - tomb, chest #1Chest after Oceiros fight
CKG: Titanite Scale - tomb, chest #2Chest after Oceiros fight
CKG: Wood Grain Ring+1 - by first elevator bottomBehind the first elevator going down into the garden, in the toxic pool
DH: Aquamarine Dagger - castle, up stairsUp the second flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Black Firebomb - ruins, up windmill from bonfireTo the left of the Earthen Peak Ruins bonfire, past the ruined windmill, next to many Poisonhorn bugs.
DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop downAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then dropping down after exiting the building from the last room.
DH: Desert Pyromancer Garb - ruins, by shack near cliffBehind a shack near the edge of the cliff of the area targeted by the second angel.
DH: Desert Pyromancer Gloves - swamp, far rightAfter dropping down in the poison swamp area, against the wall straight to the right.
DH: Desert Pyromancer Hood - swamp upper, tunnel endAt the end of the tunnel with Desert Pyromancy Zoey, to the right of the final branches.
DH: Desert Pyromancer Skirt - swamp right, by rootsIn the poison swamp, against a tree guarded by a few Poisonhorn bugs in the front right.
DH: Divine Blessing - library, after dropAfter the dropdown where an angel first targets you, behind you
DH: Divine Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Divine Blessing - swamp upper, building roofOn a rooftop of one of the buildings bordering the poison swamp. Can be reached by dropping down from the final tree branch and accessing the roof to the right.
DH: Ember - castle, behind spireAt the start of the area, behind a spire to the right of first drop down
DH: Ember - pantry, behind crates just before upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room end of the building to the right, up stairs past an illusory wall to the left, in the second-to-last room of the sequence, behind some crates to the left.
DH: Ember - ruins, alcove before swampIn an alcove providing cover from the second angel's projectiles, before dropping down in the poison swamp area.
DH: Ember - ruins, alcove on cliffIn the area with the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, dropping down to the right of the bonfire.
DH: Ember - shopSold by Stone-humped Hag, or in her ashes
DH: Flame Fan - swamp upper, NPC dropDropped by Desert Pyromancer Zoey
DH: Giant Door Shield - ruins, path below far shackDescending down a path from the edge of the cliff of the area targeted by the second angel, to the very end of the cliff.
DH: Great Soul Dregs - pantry upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then all the way to the end of the last room.
DH: Harald Curved Greatsword - swamp left, under rootIn the back leftmost area of the poison swamp, underneath the tree branch leading up and out, guarded by a stationary Harald Legion Knight.
DH: Hidden Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Homeward Bone - end of path from churchImmediately before dropping into the area with the Earthen Peak Ruins bonfire, next to Gael's flag.
DH: Homeward Bone - swamp left, on rootAll the way to the end of a short path in the back leftmost area of the poison swamp, where you can plunge attack the stationary Harald Legion Knight.
DH: Large Soul of a Weary Warrior - parapets, hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, in a corner to the left.
DH: Large Soul of a Weary Warrior - swamp centerIn the middle of the poison swamp.
DH: Large Soul of a Weary Warrior - swamp, under overhangIn the cavern adjacent to the poison swamp, surrounded by a few Poisonhorn bugs.
DH: Lightning Urn - wall outside churchAfter the dropdown where an angel first targets you, against the wall on the left.
DH: Loincloth - swamp, left edgeIn the leftmost edge of the poison swamp after dropping down, guarded by 6 Poisonhorn bugs.
DH: Lothric War Banner - parapets, end of hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, at the end of the hallway to the right.
DH: Murky Hand Scythe - library, behind bookshelvesAfter the first long drop into the building which looks like Grand Archives, to the left up the bookshelf stairs and behind the bookshelves
DH: Murky Longstaff - pantry, last roomAfter exiting the building with the Lothric Knights where the front crumbles, in the third-furthest room in the building to the right.
DH: Prism Stone - swamp upper, tunnel startNear the start of the tunnel with Desert Pyromancer Zoey.
DH: Projected Heal - parapets balconyAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman, against a wall in the area with the Lothric War Banner Knight and many murkmen.
DH: Purple Moss Clump - swamp shackIn the ruined shack with Poisonhorn bugs straight ahead of the dropdown into the poison swamp area.
DH: Ring of Favor+3 - swamp right, up rootUp the long branch close to the dropdown into the poison swamp area, in front of the cavern.
DH: Ring of Steel Protection+3 - ledge before churchAfter the dropdown where an angel first targets you, on an exposed edge to the left. Difficult to get without killing the angel.
DH: Rusted Coin - behind fountain after churchAfter exiting the building with the Lothric Knights where the front crumbles, behind the fountain on the right side.
DH: Rusted Gold Coin - shopSold by Stone-humped Hag, or in her ashes
DH: Siegbräu - LappGiven by Lapp after collecting the Titanite Slab in Earthen Peak Ruins, or left after Demon Princes fight, or dropped upon death if not given.
DH: Small Envoy Banner - boss dropFound in the small room after beating Demon Prince.
DH: Soul of a Crestfallen Knight - church, altarIn the building where the front crumbles, guarded by the two Lothric Knights at the front of the chapel.
DH: Soul of a Weary Warrior - castle overhangThe bait item at the start of the area which falls down with you into the ruined building below.
DH: Soul of the Demon PrinceDropped by Demon Prince
DH: Splitleaf Greatsword - shopSold by Stone-humped Hag, or in her ashes
DH: Titanite Chunk - castle, up stairsUp first flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Titanite Chunk - pantry, first roomAfter exiting the building with the Lothric Knights where the front crumbles, on a ledge in the first room of the building to the right.
DH: Titanite Chunk - path from church, by pillarBefore dropping into the area with the Earthen Peak Ruins bonfire, behind a pillar in front of a murkman pool.
DH: Titanite Chunk - ruins, by far shackIn front of a shack at the far edge of the cliff of the area targeted by the second angel. There is a shortcut dropdown to the left of the building.
DH: Titanite Chunk - ruins, path from bonfireAt the Earthen Peak Ruins bonfire, straight a bit then all the way left, near the edge of the cliff in the area targeted by the second angel.
DH: Titanite Chunk - swamp right, drop partway up rootPartway up the long branch close to the dropdown into the poison swamp area, in front of the cavern, dropping down to a branch to the left.
DH: Titanite Chunk - swamp, along buildingsAfter dropping down into the poison swamp, along the buildings on the left side.
DH: Titanite Chunk - swamp, path to upperPartway up the branch that leads out of the poison swamp, on a very exposed branch jutting out to the left.
DH: Titanite Scale - library, back of roomAfter the first long drop into the building which looks like Grand Archives, behind you at the back of the room
DH: Titanite Scale - swamp upper, drop and jump into towerAt the very end of the last tree branch before dropping down toward the Within Earthen Peak Ruins bonfire, drop down to the left instead. Make a jump into the interior of the overturned tower to the left.
DH: Titanite Slab - swamp, path under overhangDeep within the cavern adjacent to the poison swamp, to the back and then left. Alternatively, given by Lapp after exhausting dialogue near the bonfire and dying, or left after he moves on, or dropped upon death if not given.
DH: Twinkling Titanite - library, chandelierAfter the first long drop into the building which looks like Grand Archives, straight ahead hanging from a chandelier on the ground
DH: Twinkling Titanite - path after church, mob dropDropped the pilgrim responsible for the first angel encountered, below the spire bridge that forms by crashing into the building.
DH: Twinkling Titanite - ruins, alcove on cliff, mob dropDropped by the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, or dropping down to the right of the bonfire.
DH: Twinkling Titanite - ruins, root near bonfireTreasure visible straight ahead of the Earthen Peak Ruins bonfire on a branch. Can be accessed by following the right wall from the bonfire until a point of access onto the branch is found.
DH: Twinkling Titanite - swamp upper, drop onto rootOn the final tree branches before dropping down toward the Within Earthen Peak Ruins bonfire, drop down on a smaller branch to the right. This loops back to the original branch.
DH: Twinkling Titanite - swamp upper, mob drop on roofDropped by the pilgrim responsible for the third angel in the swamp. Rather than heading left into the tunnel with Desert Pyromancy Zoey, go right onto a shack roof. Drop down onto a tree branch at the end, then drop down to another roof.
FK: Antiquated Dress - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Gloves - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Skirt - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Atonement - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Black Bow of Pharis - miniboss drop, by keep ruins near wallDropped the Elder Ghru on the left side of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Black Bug Pellet - perimeter, hill by boss doorOn the small hill to the right of the Abyss Watchers entrance, guarded by a spear-wielding Ghru Grunt
FK: Cinders of a Lord - Abyss WatcherDropped by Abyss Watchers
FK: Crown of Dusk - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wallHidden behind the right wall of the ritual fire before Keep Ruins
FK: Dragon Crest Shield - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Dreamchaser's Ashes - keep proper, illusory wallNear the Old Wolf of Farron bonfire, behind an illusory wall near the Crystal Lizard
FK: Ember - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Ember - perimeter, path to bossGuarded by a spear-wielding Ghru Grunt to the right of the main path leading up to Abyss Watchers
FK: Ember - upper keep, by miniboss #1Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Ember - upper keep, by miniboss #2Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Estus Shard - between Farron Keep bonfire and left islandStraight ahead from the Farron Keep bonfire to the ritual fire stairs, guarded by a slug
FK: Gold Pine Bundle - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Golden Scroll - hidden caveIn a cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Great Magic Weapon - perimeter, by door to Road of SacrificesNext to the shortcut leading from Farron Keep Perimeter back into Crucifixion Woods, past the Ravenous Crystal Lizard
FK: Greataxe - upper keep, by minibossGuarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Greatsword - ramp by keep ruins ritual islandIn the middle of the swamp, on the pair of long ramps furthest from the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Havel's Armor - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Helm - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Leggings - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Heavy Gem - upper keep, lizard on stairsDropped by the Crystal Lizard that scurries up the stairs in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Hollow Gem - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Homeward Bone - right island, behind fireBehind the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Iron Flesh - Farron Keep bonfire, right after exitIn the open in the swamp, heading straight right from Farron Keep bonfire
FK: Large Soul of a Nameless Soldier - corner of keep and right islandHidden in a corner to the right of the stairs leading up to the ritual fire from the basilisk area
FK: Large Soul of a Nameless Soldier - near wall by right islandTo the left of the stairs leading up to the ritual fire from the Basilisk area, by the keep wall
FK: Large Soul of an Unknown Traveler - by white treeOn a tree close to the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Large Titanite Shard - upper keep, lizard by wyvernDropped by the farther Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Large Titanite Shard - upper keep, lizard in openDropped by the closer Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Lightning Spear - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Lingering Dragoncrest Ring - by white tree, miniboss dropDropped by the Greater Crab patrolling the birch tree where the Giant shoots arrows
FK: Magic Stoneplate Ring+1 - between right island and wallBehind a tree in the basilisk area, heading directly right from Farron Keep bonfire
FK: Manikin Claws - Londor Pale Shade dropDropped by Londor Pale Shade when he invades near the basilisks, if Yoel or Yuria have been betrayed
FK: Nameless Knight Armor - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Gauntlets - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Helm - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Leggings - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Pharis's Hat - miniboss drop, by keep ruins near wallDropped the Elder Ghru in the back of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Poison Gem - near wall by keep ruins bridgeFrom the left of the bridge leading from the ritual fire to the Keep Ruins bonfire, guarded by the three Elder Ghru
FK: Prism Stone - by left island stairsOn an island to the left of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Purple Moss Clump - Farron Keep bonfire, around right cornerAlong the inner wall of the keep, making an immediate right from Farron Keep bonfire
FK: Purple Moss Clump - keep ruins, ritual islandClose to the ritual fire before the Keep Ruins bonfire
FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfireIn the middle of the swamp, on the pair of long ramps closest to the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Ragged Mask - Farron Keep bonfire, around left cornerAlong the inner wall of the keep, making an immediate left from Farron Keep bonfire, guarded by slugs
FK: Repair Powder - outside hidden caveAlong the keep wall in the basilisk area, outside of the cave with the Elizabeth corpse and Golden Scroll
FK: Rotten Pine Resin - left island, behind fireIn the area behind the ritual fire which is straight ahead of the Farron Keep bonfire
FK: Rotten Pine Resin - outside pavilion by left islandFrom the Farron Keep bonfire straight ahead to the pavilion guarded by the Darkwraith, just to the left of the ritual fire stairs
FK: Rusted Gold Coin - right island, behind wallHidden behind the right wall of the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Sage's Coal - pavilion by left islandIn the pavilion guarded by a Darkwraith, straight ahead from the Farron Keep bonfire to the left of the ritual fire stairs
FK: Sage's Scroll - near wall by keep ruins bonfire islandAlong the keep inner wall, heading left from the stone doors past the crab area, surrounded by many Ghru enemies
FK: Shriving Stone - perimeter, just past stone doorsPast the stone doors, on the path leading up to Abyss Watchers by the Corvians
FK: Soul of a Nameless Soldier - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Soul of a Stray Demon - upper keep, miniboss dropDropped by Stray Demon on the bridge above Farron Keep
FK: Soul of the Blood of the WolfDropped by Abyss Watchers
FK: Stone Parma - near wall by left islandAlong the inner wall of the keep, making a left from Farron Keep bonfire but before the area with the Darkwraith, guarded by a slug
FK: Sunlight Talisman - estus soup island, by ladder to keep properBy the pot of estus soup to the left of the stairs leading up to Old Wolf of Farron
FK: Titanite Scale - perimeter, miniboss dropDropped by Ravenous Crystal Lizard near the shortcut from Farron Keep back to Road of Sacrifices
FK: Titanite Shard - Farron Keep bonfire, left after exitAlong the inner wall of the keep, making a left from Farron Keep bonfire, by the second group of four slugs
FK: Titanite Shard - between left island and keep ruinsIn the swamp area with the Ghru Leaper between the Keep Ruins ritual fire and ritual fire straight ahead of Farron Keep bonfire, opposite from the keep wall
FK: Titanite Shard - by keep ruins ritual island stairsBy the stairs leading up to the Keep Ruins ritual fire from the middle of the swamp
FK: Titanite Shard - by ladder to keep properIn the swamp area close to the foot of the ladder leading to Old Wolf of Farron bonfire
FK: Titanite Shard - by left island stairsIn front of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Titanite Shard - keep ruins bonfire island, under rampUnder the ramp leading down from the Keep Ruins bonfire
FK: Titanite Shard - swamp by right islandBehind a tree patrolled by an Elder Ghru close to the ritual fire stairs
FK: Twinkling Dragon Head Stone - Hawkwood dropDropped by Hawkwood after killing him in the Abyss Watchers arena, after running up to the altar in Archdragon Peak. Twinkling Dragon Torso Stone needs to be acquired first.
FK: Twinkling Titanite - keep proper, lizardDropped by the Crystal Lizard on the balcony behind the Old Wolf of Farron bonfire
FK: Undead Bone Shard - pavilion by keep ruins bonfire islandIn a standalone pavilion down the ramp from Keep Ruins bonfire and to the right
FK: Watchdogs of Farron - Old WolfGiven by Old Wolf of Farron.
FK: Wolf Ring+1 - keep ruins bonfire island, outside buildingTo the right of the building with the Keep Ruins bonfire, when approached from the ritual fire
FK: Wolf's Blood Swordgrass - by ladder to keep properTo the left of the ladder leading up to the Old Wolf of Farron bonfire
FK: Young White Branch - by white tree #1Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Young White Branch - by white tree #2Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FS: Acid Surge - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Affinity - KarlaSold by Karla after recruiting her, or in her ashes
FS: Alluring Skull - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Arstor's Spear - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Aural Decoy - OrbeckSold by Orbeck
FS: Billed Mask - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Dress - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Fire Orb - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Flame - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Gauntlets - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Iron Armor - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Gauntlets - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Helm - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Leggings - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Leggings - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Serpent - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Blessed Weapon - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Blue Tearstone Ring - GreiratGiven by Greirat upon rescuing him from the High Wall cell
FS: Boulder Heave - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Bountiful Light - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Bountiful Sunlight - Ludleth for RosariaBoss weapon for Rosaria, available after Leonhard is killed
FS: Broken Straight Sword - gravestone after bossNear the grave after Iudex Gundyr fight
FS: Budding Green Blossom - shop after killing Creighton and AL bossSold by Handmaid after receiving Silvercat Ring item lot from Sirris and defeating Aldrich
FS: Bursting Fireball - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Caressing Tears - IrinaSold by Irina after recruiting her, or in her ashes
FS: Carthus Beacon - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Carthus Flame Arc - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Cast Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Chaos Bed Vestiges - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Chaos Storm - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Clandestine Coat - shop with Orbeck's AshesSold by Handmaid after giving Orbeck's Ashes and reloading
FS: Cleric's Candlestick - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Cracked Red Eye Orb - LeonhardGiven by Ringfinger Leonhard in Firelink Shrine after reaching Tower on the Wall bonfire
FS: Crystal Hail - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Magic Weapon - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Crystal Sage's Rapier - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Soul Spear - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Dancer's Armor - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Crown - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Enchanted Swords - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Dancer's Gauntlets - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Leggings - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dark Blade - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Dark Edge - KarlaSold by Karla after recruiting her, or in her ashes
FS: Dark Hand - Yoel/YuriaSold by Yuria
FS: Darkdrift - Yoel/YuriaDropped by Yuria upon death or quest completion.
FS: Darkmoon Longbow - Ludleth for AldrichBoss weapon for Aldrich
FS: Dead Again - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Deep Protection - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Deep Soul - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Demon's Fist - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Greataxe - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Scar - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Divine Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Divine Blessing - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Dragonscale Armor - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonscale Waistcloth - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonslayer Greataxe - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Greatshield - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Swordspear - Ludleth for NamelessBoss weapon for Nameless King
FS: Dried Finger - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: East-West Shield - tree by shrine entranceIn a tree to the left of the Firelink Shrine entrance
FS: Eastern Armor - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Gauntlets - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Helm - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Leggings - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Elite Knight Armor - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Gauntlets - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Helm - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Leggings - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Ember - Dragon Chaser's AshesSold by Handmaid after giving Dragon Chaser's Ashes
FS: Ember - Grave Warden's AshesSold by Handmaid after giving Grave Warden's Ashes
FS: Ember - GreiratSold by Greirat after recruiting him, or in his ashes
FS: Ember - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Ember - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Ember - above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
FS: Ember - path right of Firelink entranceOn a cliffside to the right of the main path leading up to Firelink Shrine, guarded by a dog
FS: Ember - shopSold by Handmaid
FS: Ember - shop for Greirat's AshesSold by Handmaid after Greirat pillages Lothric Castle and handing in ashes
FS: Embraced Armor of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Executioner Armor - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Gauntlets - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Helm - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Leggings - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Exile Armor - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Gauntlets - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Leggings - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Mask - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Faraam Helm - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Farron Dart - OrbeckSold by Orbeck
FS: Farron Dart - shopSold by Handmaid
FS: Farron Flashsword - OrbeckSold by Orbeck
FS: Farron Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Farron Hail - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Farron Ring - HawkwoodGiven by Hawkwood, or dropped upon death, after defeating Abyss Watchers.
FS: Fire Orb - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Fire Surge - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Fire Whip - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Fireball - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Firelink Armor - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Gauntlets - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Greatsword - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Firelink Helm - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Leggings - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firestorm - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Flash Sweat - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Force - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Frayed Blade - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Friede's Great Scythe - Ludleth for FriedeBoss weapon for Sister Friede
FS: Gael's Greatsword - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Gauntlets of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Gnaw - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Golden Bracelets - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Golden Crown - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Grave Key - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Great Chaos Fire Orb - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Great Combustion - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Great Farron Dart - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Great Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Great Soul Arrow - OrbeckSold by Orbeck
FS: Greatsword of Judgment - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Gundyr's Armor - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Gauntlets - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Halberd - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Gundyr's Helm - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Leggings - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Havel's Ring - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Hawkwood's Shield - gravestone after Hawkwood leavesLeft by Hawkwood after defeating Abyss Watchers, Curse-Rotted Greatwood, Deacons of the Deep, and Crystal Sage
FS: Hawkwood's Swordgrass - Andre after gesture in AP summitGiven by Andre after praying at the Dragon Altar in Archdragon Peak, after acquiring Twinkling Dragon Torso Stone.
FS: Heal - IrinaSold by Irina after recruiting her, or in her ashes
FS: Heal Aid - shopSold by Handmaid
FS: Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Heavy Soul Arrow - Yoel/YuriaSold by Yoel/Yuria
FS: Helm of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Hidden Blessing - Dreamchaser's AshesSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Patches after searching GASold by Handmaid after giving Dreamchaser's Ashes, saying where they were found
FS: Hidden Body - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hidden Weapon - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hollowslayer Greatsword - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Homeward - IrinaSold by Irina after recruiting her, or in her ashes
FS: Homeward Bone - cliff edge after bossAlong the cliff edge straight ahead of the Iudex Gundyr fight
FS: Homeward Bone - path above shrine entranceTo the right of the Firelink Shrine entrance, up a slope and before the ledge on top of a coffin
FS: Homing Crystal Soulmass - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Homing Soulmass - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Karla's Coat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Coat - kill KarlaDropped from Karla upon death
FS: Karla's Gloves - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Gloves - kill KarlaDropped from Karla upon death
FS: Karla's Pointed Hat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Pointed Hat - kill KarlaDropped from Karla upon death
FS: Karla's Trousers - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Trousers - kill KarlaDropped from Karla upon death
FS: Leggings of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Leonhard's Garb - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Gauntlets - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Trousers - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Life Ring - Dreamchaser's AshesSold by Handmaid after giving Dreamchaser's Ashes
FS: Lifehunt Scythe - Ludleth for AldrichBoss weapon for Aldrich
FS: Lift Chamber Key - LeonhardGiven by Ringfinger Leonhard after acquiring a Pale Tongue.
FS: Lightning Storm - Ludleth for NamelessBoss weapon for Nameless King
FS: Lloyd's Shield Ring - Paladin's AshesSold by Handmaid after giving Paladin's Ashes
FS: Londor Braille Divine Tome - Yoel/YuriaSold by Yuria
FS: Lorian's Armor - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Gauntlets - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Greatsword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Lorian's Helm - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Leggings - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lothric's Holy Sword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Magic Barrier - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Magic Shield - OrbeckSold by Orbeck
FS: Magic Shield - Yoel/YuriaSold by Yoel/Yuria
FS: Magic Weapon - OrbeckSold by Orbeck
FS: Magic Weapon - Yoel/YuriaSold by Yoel/Yuria
FS: Mail Breaker - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Master's Attire - NPC dropDropped by Sword Master
FS: Master's Gloves - NPC dropDropped by Sword Master
FS: Med Heal - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Millwood Knight Armor - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Gauntlets - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Helm - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Leggings - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Moaning Shield - EygonDropped by Eygon of Carim
FS: Moonlight Greatsword - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: Morion Blade - Yuria for Orbeck's AshesGiven by Yuria after giving Orbeck's Ashes after she asks you to assassinate him, after he moves to Firelink Shrine. Can be done without killing Orbeck, by completing his questline.
FS: Morne's Armor - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Gauntlets - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Great Hammer - EygonDropped by Eygon of Carim
FS: Morne's Helm - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Leggings - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Old King's Great Hammer - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Old Moonlight - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Ordained Dress - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Hood - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Trousers - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pestilent Mist - Orbeck for any scrollSold by Orbeck after giving him any scroll
FS: Poison Mist - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Pontiff's Left Eye - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Prisoner's Chain - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Profaned Greatsword - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Profuse Sweat - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Rapport - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Refined Gem - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Repair - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Repeating Crossbow - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Replenishment - IrinaSold by Irina after recruiting her, or in her ashes
FS: Ring of Sacrifice - Yuria shopSold by Yuria, or by Handmaid after giving Hollow's Ashes
FS: Rose of Ariandel - Ludleth for FriedeBoss weapon for Sister Friede
FS: Rusted Gold Coin - don't forgive PatchesGiven by Patches after not forgiving him after he locks you in the Bell Tower.
FS: Sage's Big Hat - shop after killing RS bossSold by Handmaid after defeating Crystal Sage
FS: Saint's Ring - IrinaSold by Irina after recruiting her, or in her ashes
FS: Seething Chaos - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Silvercat Ring - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Skull Ring - kill LudlethDropped by Ludleth upon death, including after placing all cinders. Note that if killed before giving Transposing Kiln, transposition is not possible.
FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spellsGiven by Orbeck after purchasing the shop items corresponding to Aural Decoy, Farron Flashsword, Spook (starting items), and Pestilent Mist (after giving one scroll).
FS: Smough's Armor - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Gauntlets - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Helm - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Leggings - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Soothing Sunlight - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Soul Arrow - OrbeckSold by Orbeck
FS: Soul Arrow - Yoel/YuriaSold by Yoel/Yuria
FS: Soul Arrow - shopSold by Handmaid
FS: Soul Greatsword - OrbeckSold by Orbeck
FS: Soul Greatsword - Yoel/YuriaSold by Yoel/Yuria after using Draw Out True Strength
FS: Soul Spear - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Soul of a Deserted Corpse - bell tower doorNext to the door requiring the Tower Key
FS: Spook - OrbeckSold by Orbeck
FS: Storm Curved Sword - Ludleth for NamelessBoss weapon for Nameless King
FS: Sunless Armor - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Gauntlets - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Leggings - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Talisman - Sirris, kill GA bossDropped by Sirris on death or quest completion.
FS: Sunless Veil - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunlight Spear - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Sunset Shield - by grave after killing Hodrick w/SirrisLeft by Sirris upon quest completion.
FS: Tears of Denial - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Titanite Scale - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Titanite Slab - shop after placing all CindersSold by Handmaid after placing all Cinders of a Lord on their thrones
FS: Tower Key - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Twinkling Titanite - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Twisted Wall of Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Uchigatana - NPC dropDropped by Sword Master
FS: Undead Legion Armor - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Gauntlet - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Helm - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Leggings - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Untrue Dark Ring - Yoel/YuriaSold by Yuria
FS: Untrue White Ring - Yoel/YuriaSold by Yuria
FS: Vordt's Great Hammer - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Vow of Silence - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Washing Pole - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: White Dragon Breath - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: White Sign Soapstone - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Wolf Knight's Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Wolf Ring+2 - left of boss room exitAfter Iudex Gundyr on the left
FS: Wolnir's Crown - shop after killing CC bossSold by Handmaid after defeating High Lord Wolnir
FS: Wolnir's Holy Sword - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Wood Grain Ring - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Xanthous Gloves - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Overcoat - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Trousers - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Yhorm's Great Machete - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Yhorm's Greatshield - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Young Dragon Ring - Orbeck for one scroll and buying three spellsGiven by Orbeck after purchasing four sorceries from him, and giving him one scroll, as a non-sorcerer.
FSBT: Armor of the Sun - crow for SiegbräuTrade Siegbräu with crow
FSBT: Blessed Gem - crow for Moaning ShieldTrade Moaning Shield with crow
FSBT: Covetous Silver Serpent Ring - illusory wall past raftersFrom the Firelink Shrine roof, past the rafters and an illusory wall
FSBT: Estus Ring - tower baseDropping down from the Bell Tower to where Irina eventually resides
FSBT: Estus Shard - raftersIn the Firelink Shrine rafters, accessible from the roof
FSBT: Fire Keeper Gloves - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Robe - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Skirt - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Soul - tower topAt the top of the Bell Tower
FSBT: Hello Carving - crow for Alluring SkullTrade Alluring Skull with crow
FSBT: Help me! Carving - crow for any sacred chimeTrade any Sacred Chime with crow
FSBT: Hollow Gem - crow for EleonoraTrade Eleonora with crow
FSBT: Homeward Bone - roofOn Firelink Shrine roof
FSBT: I'm sorry Carving - crow for Shriving StoneTrade Shriving Stone with crow
FSBT: Iron Bracelets - crow for Homeward BoneTrade Homeward Bone with crow
FSBT: Iron Helm - crow for Lightning UrnTrade Lightning Urn with crow
FSBT: Iron Leggings - crow for Seed of a Giant TreeTrade Seed of a Giant Tree with crow
FSBT: Large Titanite Shard - crow for FirebombTrade Firebomb or Rope Firebomb with crow
FSBT: Lightning Gem - crow for Xanthous CrownTrade Xanthous Crown with crow
FSBT: Lucatiel's Mask - crow for Vertebra ShackleTrade Vertebra Shackle with crow
FSBT: Porcine Shield - crow for Undead Bone ShardTrade Undead Bone Shard with crow
FSBT: Ring of Sacrifice - crow for Loretta's BoneTrade Loretta's Bone with crow
FSBT: Sunlight Shield - crow for Mendicant's StaffTrade Mendicant's Staff with crow
FSBT: Thank you Carving - crow for Hidden BlessingTrade Hidden Blessing with crow
FSBT: Titanite Chunk - crow for Black FirebombTrade Black Firebomb or Rope Black Firebomb with crow
FSBT: Titanite Scale - crow for Blacksmith HammerTrade Blacksmith Hammer with crow
FSBT: Titanite Slab - crow for Coiled Sword FragmentTrade Coiled Sword Fragment with crow
FSBT: Twinkling Titanite - crow for Large Leather ShieldTrade Large Leather Shield with crow
FSBT: Twinkling Titanite - crow for Prism StoneTrade Prism Stone with crow
FSBT: Twinkling Titanite - lizard behind FirelinkDropped by the Crystal Lizard behind Firelink Shrine. Can be accessed with tree jump by going all the way around the roof, left of the entrance to the rafters, or alternatively dropping down from the Bell Tower.
FSBT: Very good! Carving - crow for Divine BlessingTrade Divine Blessing with crow
GA: Avelyn - 1F, drop from 3F onto bookshelvesOn top of a bookshelf on the Archive first floor, accessible by going halfway up the stairs to the third floor, dropping down past the Grand Archives Scholar, and then dropping down again
GA: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
GA: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
GA: Blessed Gem - raftersOn the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Chaos Gem - dark room, lizardDropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Cinders of a Lord - Lothric PrinceDropped by Twin Princes
GA: Crystal Chime - 1F, path from wax poolOn the Archives first floor, in the room with the Lothric Knight, to the right
GA: Crystal Gem - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Crystal Scroll - 2F late, miniboss dropDropped by the Grand Archives Crystal Sage
GA: Divine Blessing - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Divine Pillars of Light - cage above raftersIn a cage above the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Ember - 5F, by entranceOn a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, on the opposite side from the wax pool
GA: Estus Shard - dome, far balconyOn the Archives roof near the three Winged Knights, in a side area overlooking the ocean.
GA: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Fleshbite Ring - up stairs from 4FFrom the first shortcut elevator with the movable bookshelf, past the Scholars right before going outside onto the roof, in an alcove to the right with many Clawed Curse bookshelves
GA: Golden Wing Crest Shield - outside 5F, NPC dropDropped by Lion Knight Albert before the stairs leading up to Twin Princes
GA: Heavy Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Hollow Gem - rooftops lower, in hallGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, in a tunnel underneath the ledge
GA: Homeward Bone - 2F early balconyOn the Archives second floor, on the balcony with the ladder going up to the Crystal Sage
GA: Hunter's Ring - dome, very topAt the top of the ladder in roof the area with the Winged Knights
GA: Large Soul of a Crestfallen Knight - 4F, backIn the back of a Clawed Curse-heavy corridor of bookshelves, in the area with the Grand Archives Scholars and dropdown ladder, after the first shortcut elevator with the movable bookshelf
GA: Large Soul of a Crestfallen Knight - outside 5FIn the middle of the area with the three human NPCs attacking you, before the Grand Archives bonfire shortcut elevator
GA: Lingering Dragoncrest Ring+2 - dome, room behind spireNear the tower with the Winged Knights, up the stairs on the opposite side from the ladder leading up to the Hunter's Ring
GA: Onikiri and Ubadachi - outside 5F, NPC dropDropped by Black Hand Kamui before the stairs leading up to Twin Princes
GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Power Within - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
GA: Refined Gem - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Sage Ring+1 - rafters, second level downOn the rafters high above the Grand Archives, dropping down from the cage to the high rafters to the rafters below with the Corpse-grub
GA: Sage's Crystal Staff - outside 5F, NPC dropDropped by Daughter of Crystal Kriemhild before the stairs leading up to Twin Princes
GA: Scholar Ring - 2F, between late and earlyOn the corpse of a sitting Archives Scholar between two bookshelves, accessible by activating a lever before crossing the bridge that is the Crystal Sage's final location
GA: Sharp Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Shriving Stone - 2F late, by ladder from 3FGoing from the Crystal Sage's location on the third floor to its location on the bridge, after descending the ladder
GA: Soul Stream - 3F, behind illusory wallPast the Crystal Sage's third floor location, an illusory wall, and an Outrider Knight, on the corpse of a sitting Archives Scholar
GA: Soul of a Crestfallen Knight - 1F, loop left after dropOn the Archives first floor, hugging the left wall, on a ledge that loops back around to the left wall
GA: Soul of a Crestfallen Knight - path to domeOn balcony of the building with the second shortcut elevator down to the bonfire, accessible by going up the spiral stairs to the left
GA: Soul of a Nameless Soldier - dark roomOn the Archives first floor, after the wax pool, against a Clawed Curse bookshelf
GA: Soul of a Weary Warrior - rooftops, by lizardsOn the Archives roof, going up the first rooftop slope where a Gargoyle always attacks you
GA: Soul of the Twin PrincesDropped by Twin Princes
GA: Titanite Chunk - 1F, balconyOn the Archives first floor, on balcony overlooking the entrance opposite from the Grand Archives Scholars wax pool
GA: Titanite Chunk - 1F, path from wax poolOn the Archives first floor, toward the Lothric Knight, turning right to a ledge leading back to the entrance area
GA: Titanite Chunk - 1F, up right stairsGoing right after entering the Archives entrance and up the short flight of stairs
GA: Titanite Chunk - 2F, by wax poolUp the stairs from the Archives second floor on the right side from the entrance, in a corner near the small wax pool
GA: Titanite Chunk - 2F, right after dark roomExiting from the dark room with the Crystal Lizards on the first floor onto the second floor main room, then taking an immediate right
GA: Titanite Chunk - 5F, far balconyOn a balcony outside where Lothric Knight stands on the top floor of the Archives, accessing by going right from the final wax pool or by dropping down from the gargoyle area
GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right
GA: Titanite Chunk - rooftops lower, ledge by buttressGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, on a roof ledge to the right
GA: Titanite Chunk - rooftops, just before 5FOn the Archives roof, after a short dropdown, in the small area where the two Gargoyles attack you
GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizardDropped by a Crystal Lizard on first floor bookshelves. Can be accessed by dropping down to the left at the end of the bridge which is the Crystal Sage's final location
GA: Titanite Scale - 1F, up stairs on bookshelfOn the Archives first floor, up a movable set of stairs near the large wax pool, on top of a bookshelf
GA: Titanite Scale - 2F, titanite scale atop bookshelfOn top of a bookshelf on the Archive second floor, accessible by going halfway up the stairs to the third floor and dropping down near a Grand Archives Scholar
GA: Titanite Scale - 3F, by ladder to 2F lateGoing from the Crystal Sage's location on the third floor to its location on the bridge, on the left side of the ladder you descend, behind a table
GA: Titanite Scale - 3F, corner up stairsFrom the Grand Archives third floor up past the thralls, in a corner with bookshelves to the left
GA: Titanite Scale - 5F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof
GA: Titanite Scale - dark room, upstairsRight after going up the stairs to the Archives second floor, on the left guarded by a Grand Archives Scholar and a sequence of Clawed Curse bookshelves
GA: Titanite Scale - rooftops lower, path to 2FGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, then going past the corvians all the way to the left and making a jump
GA: Titanite Slab - 1F, after pulling 2F switchIn a chest on the Archives first floor, behind a bookshelf moved by pulling a lever in the middle of the second floor between two cursed bookshelves
GA: Titanite Slab - dome, kill all mobsDropped by killing all three Winged Knights on top of the Archives
GA: Titanite Slab - final elevator secretAt the bottom of the shortcut elevator right outside the Twin Princes fight. Requires sending the elevator up to the top from the middle, and then riding the lower elevator down.
GA: Twinkling Titanite - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Twinkling Titanite - 2F, lizard by entranceDropped by the Crystal Lizard on the Archives second floor, going toward the stairs/balcony
GA: Twinkling Titanite - dark room, lizard #1Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - dark room, lizard #2Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Twinkling Titanite - rooftops, lizard #1Dropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - rooftops, lizard #2Dropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Undead Bone Shard - 5F, by entranceOn the corpse of a sitting Archives Scholar on a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, near the final wax pool
GA: Witch's Locks - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
HWL: Astora Straight Sword - fort walkway, drop downIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel, dropping down past the edge
HWL: Basin of Vows - EmmaDropped by Emma upon killing her. This is possible to do at any time
HWL: Battle Axe - flame tower, mimicDropped by mimic in the building guarded by the fire-breathing wyvern
HWL: Binoculars - corpse tower, upper platformIn the area with the dead wyvern, at the top of a set of stairs past a Hollow Soldier
HWL: Black Firebomb - small roof over fountainAfter roof with Pus of Man, on the edge of another rooftop to the left where you can drop down into Winged Knight area
HWL: Broadsword - fort, room off walkwayIn the building with the Pus of Man on the roof, past the Lothric Knight in an alcove to the left
HWL: Cell Key - fort ground, down stairsIn the basement of the building with Pus of Man on the roof, down the stairs guarded by a dog
HWL: Claymore - flame plazaIn the area where the wyvern breathes fire, farthest away from the door
HWL: Club - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Ember - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Ember - fountain #1In the area with the Winged Knight
HWL: Ember - fountain #2In the area with the Winged Knight
HWL: Estus Shard - fort ground, on anvilIn the basement of the building with the Pus of Man on the roof, on the blacksmith anvil
HWL: Firebomb - corpse tower, under tableIn the building near the dead wyvern, behind a table near the ladder you descend
HWL: Firebomb - fort roofNext to the Pus of Man on the roof
HWL: Firebomb - top of ladder to fountainBy the long ladder leading down to the area with the Winged Knight
HWL: Firebomb - wall tower, beamIn the building with the Tower on the Wall bonfire, on a wooden beam overhanging the lower levels
HWL: Fleshbite Ring+1 - fort roof, jump to other roofJumping from the roof with the Pus of Man to a nearby building with a fenced roof
HWL: Gold Pine Resin - corpse tower, dropDropping past the dead wyvern, down the left path from the High Wall bonfire
HWL: Green Blossom - fort walkway, hall behind wheelIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel
HWL: Green Blossom - shortcut, lower courtyardIn the courtyard at the bottom of the shortcut elevator
HWL: Large Soul of a Deserted Corpse - flame plazaIn the area where the wyvern breathes fire, behind one of the praying statues
HWL: Large Soul of a Deserted Corpse - fort roofOn the edge of the roof with the Pus of Man
HWL: Large Soul of a Deserted Corpse - platform by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area)
HWL: Longbow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Lucerne - promenade, side pathOn one of the side paths from the main path connecting Dancer and Vordt fights, patrolled by a Lothric Knight
HWL: Mail Breaker - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire on the roof, before Greirat's cell
HWL: Rapier - fountain, cornerIn a corner in the area with the Winged Knight
HWL: Raw Gem - fort roof, lizardDropped by the Crystal Lizard on the rooftop after the Tower on the Wall bonfire
HWL: Red Eye Orb - wall tower, minibossDropped by the Darkwraith past the Lift Chamber Key
HWL: Refined Gem - promenade minibossDropped by the red-eyed Lothric Knight to the left of the Dancer's room entrance
HWL: Ring of Sacrifice - awning by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area), jumping onto a wooden support
HWL: Ring of the Evil Eye+2 - fort ground, far wallIn the basement of the building with the Pus of Man on the roof, on the far wall past the stairwell, behind some barrels
HWL: Silver Eagle Kite Shield - fort mezzanineIn the chest on the balcony overlooking the basement of the building with the Pus of Man on the roof
HWL: Small Lothric Banner - EmmaGiven by Emma, or dropped upon death
HWL: Soul of Boreal Valley VordtDropped by Vordt of the Boreal Valley
HWL: Soul of a Deserted Corpse - by wall tower doorRight before the entrance to the building with the Tower on the Wall bonfire
HWL: Soul of a Deserted Corpse - corpse tower, bottom floorDown the ladder of the building near the dead wyvern, on the way to the living wyvern
HWL: Soul of a Deserted Corpse - fort entry, cornerIn the corner of the room with a Lothric Knight, with the Pus of Man on the roof
HWL: Soul of a Deserted Corpse - fountain, path to promenadeIn between the Winged Knight area and the Dancer/Vordt corridor
HWL: Soul of a Deserted Corpse - path to back tower, by lift doorWhere the Greataxe Hollow Soldier patrols outside of the elevator shortcut entrance
HWL: Soul of a Deserted Corpse - path to corpse towerAt the very start, heading left from the High Wall bonfire
HWL: Soul of a Deserted Corpse - wall tower, right of exitExiting the building with the Tower on the Wall bonfire on the roof, immediately to the right
HWL: Soul of the DancerDropped by Dancer of the Boreal Valley
HWL: Standard Arrow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Throwing Knife - shortcut, lift topAt the top of the elevator shortcut, opposite from the one-way door
HWL: Throwing Knife - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire, in the room with the explosive barrels
HWL: Titanite Shard - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Titanite Shard - fort ground behind cratesBehind some wooden crates in the basement of the building with the Pus of Man on the roof
HWL: Titanite Shard - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Titanite Shard - fort, room off entryIn the building with the Pus of Man on the roof, in a room to the left and up the short stairs
HWL: Titanite Shard - wall tower, corner by bonfireOn the balcony with the Tower on the Wall bonfire
HWL: Undead Hunter Charm - fort, room off entry, in potIn the building with the Pus of Man on the roof, in a room to the left, in a pot you have to break
HWL: Way of Blue - EmmaGiven by Emma or dropped upon death.
IBV: Blood Gem - descent, platform before lakeIn front of the tree in the courtyard before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Blue Bug Pellet - ascent, in last buildingIn the final building before Pontiff's cathedral, coming from the sewer, on the first floor
IBV: Blue Bug Pellet - descent, dark roomIn the dark area with the Irithyllian slaves, to the left of the staircase
IBV: Budding Green Blossom - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Chloranthy Ring+1 - plaza, behind altarIn the area before and below Pontiff's cathedral, behind the central structure
IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark roomAfter the dark area with the Irithyllian slaves, drop down to the right
IBV: Creighton's Steel Mask - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Divine Blessing - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Divine Blessing - great hall, mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Dorhys' Gnawing - Dorhys dropDropped by Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches and to the left
IBV: Dragonslayer's Axe - Creighton dropFollowing Sirris' questline, dropped by Creighton the Wanderer when he invades in the graveyard after the Church of Yorshka.
IBV: Dung Pie - sewer #1In the area with the sewer centipedes
IBV: Dung Pie - sewer #2In the area with the sewer centipedes
IBV: Ember - shortcut from church to cathedralAfter the gate shortcut from Church of Yorshka to Pontiff's cathedral
IBV: Emit Force - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Excrement-covered Ashes - sewer, by stairsIn the area with the sewer centipedes, before going up the stairs to the kitchen
IBV: Fading Soul - descent, cliff edge #1In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Fading Soul - descent, cliff edge #2In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Great Heal - lake, dead Corpse-GrubOn the Corpse-grub at the edge of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake wallOn the wall of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake, by Distant ManorIn the lake close to the Distant Manor bonfire
IBV: Green Blossom - lake, by stairs from descentGoing down the stairs into the lake leading to the Distant Manor bonfire
IBV: Homeward Bone - descent, before gravestoneIn the graveyard down the stairs from the Church of Yorshka, in front of the grave with the Corvian
IBV: Kukri - descent, side pathDown the stairs from the graveyard after Church of Yorshka, before the group of dogs in the left path
IBV: Large Soul of a Nameless Soldier - ascent, after great hallBy the tree near the stairs from the sewer leading up to Pontiff's cathedral, where the first dogs attack you
IBV: Large Soul of a Nameless Soldier - central, by bonfireBy the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - lake islandOn an island in the lake leading to the Distant Manor bonfire
IBV: Large Soul of a Nameless Soldier - stairs to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka
IBV: Large Titanite Shard - Distant Manor, under overhangUnder overhang next to second set of stairs leading from Distant Manor bonfire
IBV: Large Titanite Shard - ascent, by elevator doorOn the path from the sewer leading up to Pontiff's cathedral, to the right of the statue surrounded by dogs
IBV: Large Titanite Shard - ascent, down ladder in last buildingOutside the final building before Pontiff's cathedral, coming from the sewer, dropping down to the left before the entrance
IBV: Large Titanite Shard - central, balcony just before plazaFrom the Central Irithyll bonfire, on the balcony with the second Fire Witch.
IBV: Large Titanite Shard - central, side path after first fountainUp the stairs from the Central Irithyll bonfire, on a railing to the right
IBV: Large Titanite Shard - great hall, main floor mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Large Titanite Shard - great hall, upstairs mob drop #1One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - great hall, upstairs mob drop #2One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - path to DorhysBefore the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Large Titanite Shard - plaza, balcony overlooking ascentOn the path from Central Irithyll bonfire, instead of going left toward the Church of Yorshka, going right, on the balcony
IBV: Large Titanite Shard - plaza, by stairs to churchTo the left of the stairs leading up to the Church of Yorshka from Central Irithyll
IBV: Leo Ring - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Lightning Gem - plaza centerIn the area before and below Pontiff's cathedral, in the center guarded by the enemies
IBV: Magic Clutch Ring - plaza, illusory wallIn the area before and below Pontiff's cathedral, behind an illusory wall to the right
IBV: Mirrah Chain Gloves - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Leggings - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Mail - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Proof of a Concord Kept - Church of Yorshka altarAt the altar in the Church of Yorshka
IBV: Rime-blue Moss Clump - central, by bonfireBy the Central Irithyll bonfire
IBV: Rime-blue Moss Clump - central, past second fountainFrom the Central Irithyll bonfire, to the left before the first Fire Witch.
IBV: Ring of Sacrifice - lake, right of stairs from descentNear the sewer centipede at the start of the lake leading to the Distant Manor bonfire
IBV: Ring of the Evil Eye - AnriGiven by Anri of Astora in the Church of Yorshka, or if told of Horace's whereabouts in the Catacombs
IBV: Ring of the Sun's First Born - fall from in front of cathedralDropping down from in front of Pontiff Sulyvahn's church toward the Church of Yorshka
IBV: Roster of Knights - descent, first landingOn the landing going down the stairs from Church of Yorshka to the graveyard
IBV: Rusted Gold Coin - Distant Manor, drop after stairsDropping down after the first set of stairs leading from Distant Manor bonfire
IBV: Rusted Gold Coin - descent, side pathDown the stairs from the graveyard after Church of Yorshka, guarded by the group of dogs in the left path
IBV: Shriving Stone - descent, dark room raftersOn the rafters in the dark area with the Irithyllian slaves
IBV: Siegbräu - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Smough's Great Hammer - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Soul of Pontiff SulyvahnDropped by Pontiff Sulyvahn
IBV: Soul of a Weary Warrior - ascent, by final staircaseToward the end of the path from the sewer leading up to Pontiff's cathedral, to the left of the final staircase
IBV: Soul of a Weary Warrior - central, by first fountainBy the Central Irithyll bonfire
IBV: Soul of a Weary Warrior - central, railing by first fountainOn the railing overlooking the Central Irithyll bonfire, at the very start
IBV: Soul of a Weary Warrior - plaza, side room lowerDropping down from the path from Church of Yorshka to Pontiff, guarded by the pensive Fire Witch
IBV: Soul of a Weary Warrior - plaza, side room upperIn the path from Church of Yorshka to Pontiff's cathedral, at the broken ledge you can drop down onto the Fire Witch
IBV: Twinkling Titanite - central, lizard before plazaDropped by a Crystal Lizard past the Central Irithyll Fire Witches and to the left
IBV: Twinkling Titanite - descent, lizard behind illusory wallDropped by a Crystal Lizard behind an illusory wall before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Undead Bone Shard - descent, behind gravestoneIn the graveyard down the stairs from the Church of Yorshka, behind the grave with the Corvian
IBV: Witchtree Branch - by DorhysIn the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Wood Grain Ring+2 - ascent, right after great hallLeaving the building with the Silver Knight staring at the painting, instead of going left up the stairs, go right
IBV: Yorshka's Spear - descent, dark room rafters chestIn a chest in the rafters of the dark area with the Irithyllian slaves
ID: Alva Armor - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Gauntlets - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Helm - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Leggings - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Bellowing Dragoncrest Ring - drop from B1 towards pitDropping down from the Jailbreaker's Key shortcut at the end of the top corridor on the bonfire side in Irithyll Dungeon
ID: Covetous Gold Serpent Ring - Siegward's cellIn the Old Cell where Siegward is rescued
ID: Covetous Silver Serpent Ring+1 - pit lift, middle platformOn one of the platforms in elevator shaft of the shortcut elevator from the Giant Slave area to the Irithyll Dungeon bonfire
ID: Dark Clutch Ring - stairs between pit and B3, mimicDropped by the mimic found going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs, on the left side
ID: Dragon Torso Stone - B3, outside liftOn the balcony corpse in the Path of the Dragon pose
ID: Dragonslayer Lightning Arrow - pit, mimic in hallDropped by the mimic in the side corridor from where the Giant Slave is standing, before the long ladder
ID: Dung Pie - B3, by path from pitIn the room with the Giant Hound Rats
ID: Dung Pie - pit, miniboss dropDrop from the Giant Slave
ID: Dusk Crown Ring - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
ID: Ember - B3 centerAt the center pillar in the main Jailer cell block
ID: Ember - B3 far rightIn the main Jailer cell block, on the left side coming from the Profaned Capital
ID: Estus Shard - mimic on path from B2 to pitDropped by the mimic in the room after the outside area of Irithyll Dungeon overlooking Profaned Capital
ID: Fading Soul - B1 near, main hallOn the top corridor on the bonfire side in Irithyll Dungeon, close to the first Jailer
ID: Great Magic Shield - B2 near, mob drop in far left cellOne-time drop from the Infested Corpse in the bottom corridor on the bonfire side of Irithyll Dungeon, in the closest cell
ID: Homeward Bone - path from B2 to pitIn the part of Irithyll Dungeon overlooking the Profaned Capital, after exiting the last jail cell corridor
ID: Jailbreaker's Key - B1 far, cell after gateIn the cell of the top corridor opposite to the bonfire in Irithyll Dungeon
ID: Large Soul of a Nameless Soldier - B2 far, by liftTaking the elevator up from the area you can use Path of the Dragon, before the one-way door
ID: Large Soul of a Nameless Soldier - B2, hall by stairsAt the end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Large Soul of a Weary Warrior - just before Profaned CapitalIn the open area before the bridge leading into Profaned Capital from Irithyll Dungeon
ID: Large Titanite Shard - B1 far, rightmost cellIn a cell on the far end of the top corridor opposite to the bonfire in Irithyll Dungeon, nearby the Jailer
ID: Large Titanite Shard - B1 near, by doorAt the end of the top corridor on the bonfire side in Irithyll Dungeon, before the Jailbreaker's Key door
ID: Large Titanite Shard - B3 near, right cornerIn the main Jailer cell block, to the left of the hallway leading to the Path of the Dragon area
ID: Large Titanite Shard - after bonfire, second cell on rightIn the second cell on the right after Irithyll Dungeon bonfire
ID: Large Titanite Shard - pit #1On the floor where the Giant Slave is standing
ID: Large Titanite Shard - pit #2On the floor where the Giant Slave is standing
ID: Lightning Blade - B3 lift, middle platformOn the middle platform riding the elevator up from the Path of the Dragon area
ID: Lightning Bolt - awning over pitOn the wooden overhangs above the Giant Slave. Can be reached by dropping down after climbing the long ladder around the area where the Giant stands.
ID: Murakumo - Alva dropDropped by Alva, Seeker of the Spurned when he invades in the cliffside path to Irithyll Dungeon
ID: Old Cell Key - stairs between pit and B3In a chest found going past the Giant Slave to the sewer with the rats and the basilisks, up the stairs to the end, on the right side
ID: Old Sorcerer Boots - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Coat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Gauntlets - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Hat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Pale Pine Resin - B1 far, cell with broken wallIn the jail cell with the broken wall in the top corridor opposite to the bonfire in Irithyll Dungeon, near the passive Wretch on the wall
ID: Pickaxe - path from pit to B3Passing by the Giant Slave, before the tunnel with the rats and basilisks
ID: Prisoner Chief's Ashes - B2 near, locked cell by stairsIn the cell at the far end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Profaned Coal - B3 far, left cellIn the room with the Wretches next to the main Jailer cell block, guarded by a Wretch
ID: Profaned Flame - pitOn the floor where the Giant Slave is standing
ID: Rusted Coin - after bonfire, first cell on leftIn the first cell on the left from the Irithyll dungeon bonfire
ID: Rusted Gold Coin - after bonfire, last cell on rightIn the third cell on the right from the Irithyll Dungeon bonfire
ID: Simple Gem - B2 far, cell by stairsIn the cell near the bottom corridor opposite to the bonfire in Irithyll Dungeon, adjacent to the room with three Jailers and Cage Spiders
ID: Soul of a Crestfallen Knight - balcony above pitUnder whether the Giant Slave is resting his head
ID: Soul of a Weary Warrior - by drop to pitAt the end of the room with many peasant hollows after the Estus Shard mimic
ID: Soul of a Weary Warrior - stairs between pit and B3Going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs
ID: Titanite Chunk - balcony above pit, lizardDropped by the Crystal Lizard where the Giant Slave is resting his head
ID: Titanite Chunk - pit, miniboss dropDrop from the Giant Slave
ID: Titanite Scale - B2 far, lizardDropped by the Crystal Lizard on the bottom corridor opposite from the bonfire in Irithyll Dungeon where a Wretch attacks you
ID: Titanite Scale - B3 far, mimic in hallDropped by the mimic in the main Jailer cell block
ID: Titanite Slab - SiegwardGiven by Siegward after unlocking Old Cell or on quest completion
ID: Xanthous Ashes - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
KFF: Soul of the LordsDropped by Soul of Cinder
LC: Black Firebomb - dark room lowerIn the room with the firebomb-throwing hollows, against the wall on the lowest level
LC: Braille Divine Tome of Lothric - wyvern roomIn the room next to the second Pus of Man wyvern
LC: Caitha's Chime - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, and then dropping down again towards the Lothric knights
LC: Dark Stoneplate Ring+1 - wyvern room, balconyThrough the room next to the second Pus of Man wyvern, on the balcony outside
LC: Ember - by Dragon Barracks bonfireNear the Dragon Barracks bonfire
LC: Ember - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Ember - main hall, left of stairsTo the left of the stairs past the Dragon Barracks grate
LC: Ember - plaza centerIn the area where the Pus of Man wyverns breathe fire
LC: Ember - plaza, by gateOn the railing near the area where the Pus of Man wyverns breathe fire, before the gate
LC: Ember - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Grand Archives Key - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Greatlance - overlooking Dragon Barracks bonfireGuarded by a pensive Lothric Knight after the Dragon Barracks bonfire and continuing up the stairs
LC: Hood of PrayerIn a chest right after the Lothric Castle bonfire
LC: Irithyll Rapier - basement, miniboss dropDropped by the Boreal Outrider Knight in the basement
LC: Knight's Ring - altarClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, past the Large Hollow Soldier, down into the room with the tables
LC: Large Soul of a Nameless Soldier - dark room midIn the room with the firebomb-throwing hollows, up the ladder
LC: Large Soul of a Nameless Soldier - moat, right pathFound on the ledge after dropping into the area with the Pus of Man transforming hollows and making the entire loop
LC: Large Soul of a Nameless Soldier - plaza left, by pillarIn the building to the left of the area where the Pus of Man wyverns breathe fire, against a pillar
LC: Large Soul of a Weary Warrior - ascent, last turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs and forwards
LC: Large Soul of a Weary Warrior - main hall, by leverOn a ledge to the right of the lever opening the grate
LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop downPast the room with the firebomb-throwing hollows and Pus of Man wyvern, around to the front, dropping down past where the Titanite Chunk is
LC: Lightning Urn - moat, right path, first roomStarting the loop from where the Pus of Man hollows transform, behind some crates in the first room
LC: Lightning Urn - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Pale Pine Resin - dark room upper, by mimicIn the room with the firebomb-throwing hollows, next to the mimic in the far back left
LC: Raw Gem - plaza leftOn a balcony to the left of the area where the Pus of Man wyverns breathe fire, where the Hollow Soldier throws Undead Hunter Charms
LC: Red Tearstone Ring - chapel, drop onto roofFrom the chapel to the right of the Dragonslayer Armour fight, on the balcony to the left
LC: Refined Gem - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Robe of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Rusted Coin - chapelIn the chapel to the right of the Dragonslayer Armour fight
LC: Sacred Bloom Shield - ascent, behind illusory wallUp the ladder where the Winged Knight is waiting, past an illusory wall
LC: Skirt of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Sniper Bolt - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Sniper Crossbow - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Soul of Dragonslayer ArmourDropped by Dragonslayer Armour
LC: Soul of a Crestfallen Knight - by lift bottomGuarded by a buffed Lothric Knight straight from the Dancer bonfire
LC: Soul of a Crestfallen Knight - wyvern room, balconyOn a ledge accessible after the second Pus of Man wyvern is defeated
LC: Spirit Tree Crest Shield - basement, chestIn a chest in the basement with the Outrider Knight
LC: Sunlight Medal - by lift topNext to the shortcut elevator outside of the Dragonslayer Armour fight that goes down to the start of the area
LC: Sunlight Straight Sword - wyvern room, mimicDropped by the mimic in the room next to the second Pus of Man wyvern
LC: Thunder Stoneplate Ring+2 - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, out on the edge
LC: Titanite Chunk - altar roofClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, overlooking the tree
LC: Titanite Chunk - ascent, final turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs, then right
LC: Titanite Chunk - ascent, first balconyRight after the Lothric Castle bonfire, out on the balcony
LC: Titanite Chunk - ascent, turret before barricadesFrom the Lothric Castle bonfire, up the stairs, straight, and then down the stairs behind the barricade
LC: Titanite Chunk - dark room mid, out door opposite wyvernFrom the room with the firebomb-throwing hollows, past the Pus of Man Wyvern and back around the front, before the Crystal Lizard
LC: Titanite Chunk - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Titanite Chunk - down stairs after bossDown the stairs to the right after Dragonslayer Armour
LC: Titanite Chunk - moat #1In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat #2In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat, near ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the near side to the bonfire
LC: Titanite Chunk - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Titanite Scale - altarIn a chest climbing the ladder to the rooftop outside the Dragonslayer Armour fight, continuing the loop past the Red-Eyed Lothric Knight
LC: Titanite Scale - basement, chestIn a chest in the basement with the Outrider Knight
LC: Titanite Scale - chapel, chestIn a chest in the chapel to the right of the Dragonslayer Armour fight
LC: Titanite Scale - dark room mid, out door opposite wyvernPassing through the room with the firebomb-throwing hollows and the Pus of Man wyvern around to the front, overlooking the area where the wyverns breathe fire
LC: Titanite Scale - dark room, upper balconyIn the room with the firebomb-throwing hollows, at the very top on a balcony to the right
LC: Titanite Scale - dark room, upper, mimicDropped by the crawling mimic at the top of the room with the firebomb-throwing hollows
LC: Twinkling Titanite - ascent, side roomIn the room where the Winged Knight drops down
LC: Twinkling Titanite - basement, chest #1In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - basement, chest #2In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizardDropped by the Crystal Lizard after the room with the firebomb-throwing hollows around the front
LC: Twinkling Titanite - moat, left sideBehind one of the Pus of Man transforming hollows, to the left of the bridge to the wyvern fire-breathing area
LC: Twinkling Titanite - moat, right path, lizardDropped by the Crystal Lizard near the thieves after dropping down to the area with the Pus of Man transforming hollows
LC: Undead Bone Shard - moat, far ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the far side from the bonfire
LC: Winged Knight Armor - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Gauntlets - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Helm - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Leggings - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
PC: Blooming Purple Moss Clump - walkway above swampAt the right end of the plank before dropping down into the Profaned Capital toxic pool
PC: Cinders of a Lord - Yhorm the GiantDropped by Yhorm the Giant
PC: Court Sorcerer Gloves - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Hood - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Robe - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Trousers - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer's Staff - chapel, mimic on second floorDropped by the mimic on the second floor of the Monstrosity of Sin building
PC: Cursebite Ring - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Eleonora - chapel ground floor, kill mobDropped by the Monstrosity of Sin on the first floor, furthest away from the door
PC: Ember - palace, far roomTo the right of the Profaned Flame, in the room with the many Jailers looking at the mimics
PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entranceDropping down from the roof connected to the second floor of the Monstrosity of Sin building, above the main entrance to the building
PC: Greatshield of Glory - palace, mimic in far roomDropped by the left mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Jailer's Key Ring - hall past chapelPast the Profaned Capital Court Sorcerer, in the corridor overlooking the Irithyll Dungeon Giant Slave area
PC: Large Soul of a Weary Warrior - bridge, far endOn the way from the Profaned Capital bonfire toward the Profaned Flame, crossing the bridge without dropping down
PC: Logan's Scroll - chapel roof, NPC dropDropped by the court sorcerer above the toxic pool
PC: Magic Stoneplate Ring+2 - tower baseAt the base of the Profaned Capital structure, going all the way around the outside wall clockwise
PC: Onislayer Greatarrow - bridgeItem on the bridge descending from the Profaned Capital bonfire into the Profaned Flame building
PC: Onislayer Greatbow - drop from bridgeFrom the bridge leading from the Profaned Capital bonfire to Yhorm, onto the ruined pillars shortcut to the right, behind you after the first dropdown.
PC: Pierce Shield - SiegwardDropped by Siegward upon death or quest completion, and sold by Patches while Siegward is in the well.
PC: Poison Arrow - chapel roofAt the far end of the roof with the Court Sorcerer
PC: Poison Gem - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Purging Stone - chapel ground floorAt the back of the room with the three Monstrosities of Sin on the first floor
PC: Purging Stone - swamp, by chapel ladderIn the middle of the Profaned Capital toxic pool, near the ladder to the Court Sorcerer
PC: Rubbish - chapel, down stairs from second floorHanging corpse visible from Profaned Capital accessible from the second floor of the building with the Monstrosities of Sin, in the back right
PC: Rusted Coin - below bridge #1Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - below bridge #2Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - tower exteriorTreasure visible on a ledge in the Profaned Capital bonfire. Can be accessed by climbing a ladder outside the main structure.
PC: Rusted Gold Coin - halls above swampIn the corridors leading to the Profaned Capital toxic pool
PC: Rusted Gold Coin - palace, mimic in far roomDropped by the right mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Shriving Stone - swamp, by chapel doorAt the far end of the Profaned Capital toxic pool, to the left of the door leading to the Monstrosities of Sin
PC: Siegbräu - Siegward after killing bossGiven by Siegward after helping him defeat Yhorm the Giant. You must talk to him before Emma teleports you.
PC: Soul of Yhorm the GiantDropped by Yhorm the Giant
PC: Storm Ruler - SiegwardDropped by Siegward upon death or quest completion.
PC: Storm Ruler - boss roomTo the right of Yhorm's throne
PC: Twinkling Titanite - halls above swamp, lizard #1Dropped by the second Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Twinkling Titanite - halls above swamp, lizard #2Dropped by the first Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Undead Bone Shard - by bonfireOn the corpse of Laddersmith Gilligan next to the Profaned Capital bonfire
PC: Wrath of the Gods - chapel, drop from roofDropping down from the roof of the Monstrosity of Sin building where the Court Sorcerer is
PW1: Black Firebomb - snowfield lower, path to bonfireDropping down after the first snow overhang and following the wall on the left, past the rotting bed descending toward the second bonfire
PW1: Blessed Gem - snowfield, behind towerBehind the Millwood Knight tower in the first area, approach from the right side
PW1: Budding Green Blossom - settlement courtyard, ledgeAfter sliding down the slope on the way to Corvian Settlement, dropping down hugging the left wall
PW1: Captain's Ashes - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder
PW1: Chillbite Ring - FriedeGiven by Sister Friede while she is sitting in the Ariandel Chapel, or on the stool after she moves.
PW1: Contraption Key - library, NPC dropDropped by Sir Vilhelm
PW1: Crow Quills - settlement loop, jump into courtyardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Go right and jump past some barrels onto the central platform.
PW1: Crow Talons - settlement roofs, near bonfireAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, into the building, then looping around onto its roof.
PW1: Dark Gem - settlement back, egg buildingDropping down to the right of the gate guarded by a Corvian Knight in Corvian Settlement, inside of the last building on the right
PW1: Ember - roots above depthsIn the tree branch area after climbing down the rope bridge, hugging a right wall past a Follower Javelin wielder
PW1: Ember - settlement main, left building after bridgeCrossing the bridge after Corvian Settlement bonfire, in the building to the left.
PW1: Ember - settlement, building near bonfireIn the first building in Corvian Settlement next to the bonfire building
PW1: Ethereal Oak Shield - snowfield tower, 3FIn the Millwood Knight tower on a Millwood Knight corpse, after climbing the first ladder, then going down the staircase
PW1: Follower Javelin - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Follower Sabre - roots above depthsOn a tree branch after climbing down the rope bridge. Rather than hugging a right wall toward a Follower Javelin wielder, drop off to the left.
PW1: Frozen Weapon - snowfield lower, egg zoneDropping down after the first snow overhang, in the rotting bed along the left side
PW1: Heavy Gem - snowfield villageBefore the Millwood Knight tower, on the far side of one of the ruined walls targeted by the archer
PW1: Hollow Gem - beside chapelTo the right of the entrance to the Ariandel
PW1: Homeward Bone - depths, up hillIn the Depths of the Painting, up a hill next to the giant crabs.
PW1: Homeward Bone - snowfield village, outcroppingDropping down after the first snow overhang and following the cliff on the right, making a sharp right after a ruined wall segment before approaching the Millwood Knight tower
PW1: Large Soul of a Weary Warrior - settlement hall roofOn top of the chapel with the Corvian Knight to the left of Vilhelm's building
PW1: Large Soul of a Weary Warrior - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder, on a Millwood Knight corpse
PW1: Large Soul of an Unknown Traveler - below snowfield village overhangUp the slope to the left of the Millwood Knight tower, dropping down after a snow overhang, then several more ledges.
PW1: Large Soul of an Unknown Traveler - settlement backIn Corvian Settlement, on the ground before the ladder climbing onto the rooftops
PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliffAfter sliding down the slope on the way to Corvian Settlement, on a cliff to the right and behind
PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfireCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. On the corpse in a hole in the wall leading back to the bonfire.
PW1: Large Soul of an Unknown Traveler - settlement roofs, balconyAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, on the other side of the bridge.
PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfireTo the right of the ladder leading up to Corvian Settlement bonfire.
PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliffDropping down after the first snow overhang, between the forest and the cliff edge, before where the large wolf drops down
PW1: Large Soul of an Unknown Traveler - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Large Soul of an Unknown Traveler - snowfield lower, path to villageDropping down after the first snow overhang and following the cliff on the right, on a tree past where the large wolf jumps down
PW1: Large Soul of an Unknown Traveler - snowfield upperGoing straight after the first bonfire, to the left of the caving snow overhand
PW1: Large Titanite Shard - lizard under bridge nearDropped by a Crystal Lizard after the Rope Bridge Cave on the way to Corvian Settlement
PW1: Large Titanite Shard - settlement loop, lizardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Hug the bonfire building's outer wall along the right side.
PW1: Large Titanite Shard - snowfield lower, left from fallDropping down after the first snow overhang, guarded by a Tree Woman overlooking the rotting bed along the left wall
PW1: Millwood Battle Axe - snowfield tower, 5FIn the Milkwood Knight tower, either dropping down from rafters after climbing the second ladder or making a risky jump
PW1: Millwood Greatarrow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Millwood Greatbow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Onyx Blade - library, NPC dropDropped by Sir Vilhelm
PW1: Poison Gem - snowfield upper, forward from bonfireFollowing the left wall from the start, guarded by a Giant Fly
PW1: Rime-blue Moss Clump - below bridge farIn a small alcove to the right after climbing down the rope bridge
PW1: Rime-blue Moss Clump - snowfield upper, overhangOn the first snow overhang at the start. It drops down at the same time you do.
PW1: Rime-blue Moss Clump - snowfield upper, starting caveIn the starting cave
PW1: Rusted Coin - right of libraryTo the right of Vilhelm's building
PW1: Rusted Coin - snowfield lower, straight from fallDropping down after the first snow overhang, shortly straight ahead
PW1: Rusted Gold Coin - settlement roofs, roof near second ladderAfter climbing the second ladder on the Corvian Settlement rooftops, immediately dropping off the bridge to the right, on a rooftop
PW1: Shriving Stone - below bridge nearAfter the Rope Bridge Cave bonfire, dropping down before the bridge, following the ledge all the way to the right
PW1: Simple Gem - settlement, lowest level, behind gateCrossing the bridge after Corvian Settlement bonfire, follow the left edge until a bridge, then drop down on the right side. Guarded by a Sewer Centipede.
PW1: Slave Knight Armor - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Gauntlets - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Hood - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Leggings - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Snap Freeze - depths, far end, mob dropIn the Depths of the Painting, past the giant crabs, guarded by a special Tree Woman. Killing her drops down a very long nearby ladder.
PW1: Soul of a Crestfallen Knight - settlement hall, raftersIn the rafters of the chapel with the Corvian Knight to the left of Vilhelm's building. Can drop down from the windows exposed to the roof.
PW1: Soul of a Weary Warrior - snowfield tower, 1FAt the bottom of the Millwood Knight tower on a Millwood Knight corpse
PW1: Titanite Slab - CorvianGiven by the Corvian NPC in the building next to Corvian Settlement bonfire.
PW1: Titanite Slab - depths, up secret ladderIn the Depths of the Painting, past the giant crabs, killing a special Tree Woman drops down a very long nearby ladder. Climb the ladder and also the ladder after that one.
PW1: Twinkling Titanite - roots, lizardDropped by a Crystal Lizard in the tree branch area after climbing down the rope bridge, before the ledge with the Follower Javelin wielder
PW1: Twinkling Titanite - settlement roofs, lizard before hallDropped by a Crystal Lizard on a bridge in Corvian Settlement before the rooftop of the chapel with the Corvian Knight inside.
PW1: Twinkling Titanite - snowfield tower, 3F lizardDropped by a Crystal Lizard in the Millwood Knight tower, climbing up the first ladder and descending the stairs down
PW1: Valorheart - boss dropDropped by Champion's Gravetender
PW1: Way of White Corona - settlement hall, by altarIn the chapel with the Corvian Knight to the left of Vilhelm's building, in front of the altar.
PW1: Young White Branch - right of libraryTo the right of Vilhelm's building
PW2: Blood Gem - B2, centerOn the lower level of the Ariandel Chapel basement, in the middle
PW2: Dung Pie - B1On the higher level of the Ariandel Chapel basement, on a wooden beam overlooking the lower level
PW2: Earth Seeker - pit caveIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, in the cave
PW2: Ember - pass, central alcoveAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, in a small alcove along the left wall
PW2: Floating Chaos - NPC dropDropped by Livid Pyromancer Dunnel when he invades while embered, whether boss is defeated or not. On the second level of Priscilla's building above the Gravetender fight, accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Follower Shield - pass, far cliffsideAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, on the cliff ledge past the open area, to the left
PW2: Follower Torch - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going all the way down the slope on the edge of the map. Guarded by a Follower
PW2: Homeward Bone - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Large Soul of a Crestfallen Knight - pit, by treeIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, by the tree
PW2: Large Titanite Shard - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going partway down the slope on the edge of the map
PW2: Large Titanite Shard - pass, just before B1On the way to Ariandel Chapel basement, past the Millwood Knights and before the first rotten tree that can be knocked down
PW2: Prism Stone - pass, tree by beginningUp the slope and to the left after the Snowy Mountain Pass, straight ahead by a tree
PW2: Pyromancer's Parting Flame - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Quakestone Hammer - pass, side path near B1On the way to Ariandel Chapel basement, rather than going right past the two Millwood Knights, go left, guarded by a very strong Millwood Knight
PW2: Soul of Sister FriedeDropped by Sister Friede
PW2: Soul of a Crestfallen Knight - pit edge #1In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Soul of a Crestfallen Knight - pit edge #2In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Titanite Chunk - pass, by kickable treeAfter the Snowy Mountain Pass bonfire, on a ledge to the right of the slope with the bell stuck in the ground, behind a tree
PW2: Titanite Chunk - pass, cliff overlooking bonfireOn a cliff overlooking the Snowy Mountain Pass bonfire. Requires following the left wall
PW2: Titanite Slab - boss dropOne-time drop after killing Father Ariandel and Friede (phase 2) for the first time.
PW2: Twinkling Titanite - B3, lizard #1Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Twinkling Titanite - B3, lizard #2Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Vilhelm's Armor - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Gauntlets - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's HelmOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Leggings - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
RC: Antiquated Plain Garb - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Black Witch Garb - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Hat - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Trousers - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Veil - swamp near right, by sunken churchTo the left of the submerged building with 4 Ringed Knights, near a spear-wielding knight.
RC: Black Witch Wrappings - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Blessed Gem - grave, down lowest stairsIn Shared Grave, after dropping down near Gael's flag and dropping down again, behind you. Or from the bonfire, go back through the side tunnel with the skeletons and down the stairs after that.
RC: Blindfold Mask - grave, NPC dropDropped by Moaning Knight (invades whether embered or not, or boss defeated or not) in Shared Grave.
RC: Blood of the Dark Soul - end boss dropDropped by Slave Knight Gael
RC: Budding Green Blossom - church pathOn the way to the Halflight building.
RC: Budding Green Blossom - wall top, flowers by stairsIn a patch of flowers to the right of the stairs leading up to the first Judicator along the left wall of the courtyard are Mausoleum Lookout.
RC: Budding Green Blossom - wall top, in flower clusterAlong the left wall of the courtyard after Mausoleum Lookout, in a patch of flowers.
RC: Chloranthy Ring+3 - wall hidden, drop onto statueFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, dropping back down toward the way to Filianore, onto a platform with a Gwyn statue. Try to land on the platform rather than the statue.
RC: Church Guardian Shiv - swamp far left, in buildingInside of the building at the remote end of the muck pit surrounded by praying Hollow Clerics.
RC: Covetous Gold Serpent Ring+3 - streets, by LappGoing up the very long ladder from the muck pit, then up some stairs, to the left, and across the bridge, in a building past the Ringed Knights. Also where Lapp can be found to tell him of the Purging Monument.
RC: Crucifix of the Mad King - ashes, NPC dropDropped by Shira, who invades you (ember not required) in the far-future version of her room
RC: Dark Gem - swamp near, by stairsIn the middle of the muck pit, close to the long stairs.
RC: Divine Blessing - streets monument, mob dropDropped by the Judicator near the Purging Monument area. Requires solving "Show Your Humanity" puzzle.
RC: Divine Blessing - wall top, mob dropDropped by the Judicator after the Mausoleum Lookup bonfire.
RC: Dragonhead Greatshield - lower cliff, under bridgeDown a slope to the right of the bridge where Midir first assaults you, past a sword-wielding Ringed Knight, under the bridge.
RC: Dragonhead Shield - streets monument, across bridgeFound in Purging Monument area, across the bridge from the monument. Requires solving "Show Your Humanity" puzzle.
RC: Ember - wall hidden, statue roomFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, in the room with the illusory statue.
RC: Ember - wall top, by statueAlong the left wall of the courtyard after Mausoleum Lookout, in front of a tall monument.
RC: Ember - wall upper, balconyOn the balcony attached to the room with the Ringed Inner Wall bonfire.
RC: Filianore's Spear Ornament - mid boss dropDropped by Halflight, Spear of the Church
RC: Filianore's Spear Ornament - wall hidden, by ladderNext the ladder leading down to the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Havel's Ring+3 - streets high, drop from building oppositeDropping down from the building where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Hidden Blessing - swamp center, mob dropDropped by Judicator patrolling the muck pit.
RC: Hidden Blessing - wall top, tomb under platformIn a tomb underneath the platform with the first Judicator, accessed by approaching from Mausoleum Lookout bonfire.
RC: Hollow Gem - wall upper, path to towerHeading down the cursed stairs after Ringed Inner Wall bonfire and another short flight of stairs, hanging on a balcony.
RC: Iron Dragonslayer Armor - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Gauntlets - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Helm - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Leggings - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Lapp's Armor - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Gauntlets - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Helm - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Leggings - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Large Soul of a Crestfallen Knight - streets monument, across bridgeFound in Purging Monument area, on the other side of the bridge leading to the monument. Requires solving "Show Your Humanity" puzzle.
RC: Large Soul of a Crestfallen Knight - streets, far stairsToward the bottom of the stairs leading down to the muck pit.
RC: Large Soul of a Weary Warrior - lower cliff, endToward the end of the upper path attacked Midir's fire-breathing.
RC: Large Soul of a Weary Warrior - swamp center, by peninsulaIn the muck pit approaching where the Judicator patrols from the stairs.
RC: Large Soul of a Weary Warrior - wall lower, past two illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past two illusory walls on the right side of the ascending stairs.
RC: Large Soul of a Weary Warrior - wall top, right of small tombIn the open toward the end of the courtyard after the Mausoleum Lookout bonfire, on the right side of the small tomb.
RC: Ledo's Great Hammer - streets high, opposite building, NPC dropDropped by Silver Knight Ledo (invades whether embered or not, or boss defeated or not) in the building down the path to the right after climbing the very long ladder from the muck area.
RC: Lightning Arrow - wall lower, past three illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past three illusory walls on the right side of the ascending stairs.
RC: Lightning Gem - grave, room after first dropIn Shared Grave, in the first room encountered after falling down from the crumbling stairs and continuing upward.
RC: Mossfruit - streets near left, path to gardenPartway down the stairs from Shira, across the bridge.
RC: Mossfruit - streets, far left alcoveNear the bottom of the stairs before the muck pit, in an alcove to the left.
RC: Preacher's Right Arm - swamp near right, by towerIn the muck pit behind a crystal-covered structure, close to the Ringed City Streets shortcut entrance.
RC: Prism Stone - swamp near, railing by bonfireOn the balcony of the path leading up to Ringed City Streets bonfire from the muck pit.
RC: Purging Stone - wall top, by door to upperAt the end of the path from Mausoleum Lookup to Ringed Inner Wall, just outside the door.
RC: Ring of the Evil Eye+3 - grave, mimicDropped by mimic in Shared Grave. In one of the rooms after dropping down near Gael's flag and then dropping down again.
RC: Ringed Knight Paired Greatswords - church path, mob dropDropped by Ringed Knight with paired greatswords before Filianore building.
RC: Ringed Knight Spear - streets, down far right hallIn a courtyard guarded by a spear-wielding Ringed Knight. Can be accessed from a hallway filled with cursed clerics on the right side going down the long stairs, or by climbing up the long ladder from the muck pit and dropping down past the Locust Preacher.
RC: Ringed Knight Straight Sword - swamp near, tower on peninsulaOn a monument next to the Ringed City Streets building. Can be easily accessed after unlocking the shortcut by following the left wall inside and then outside the building.
RC: Ritual Spear Fragment - church pathTo the right of the Paired Greatswords Ringed Knight on the way to Halflight.
RC: Rubbish - lower cliff, middleIn the middle of the upper path attacked Midir's fire-breathing, after the first alcove.
RC: Rubbish - swamp far, by crystalIn the remote end of the muck pit, next to a massive crystal structure between a giant tree and the building with praying Hollow Clerics, guarded by several Locust Preachers.
RC: Ruin Armor - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Gauntlets - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Helm - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Leggings - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Sacred Chime of Filianore - ashes, NPC dropGiven by Shira after accepting her request to kill Midir, or dropped by her in post-Filianore Ringed City.
RC: Shira's Armor - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Crown - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Gloves - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Trousers - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shriving Stone - wall tower, bottom floor centerIn the cylindrical building before the long stairs with many Harald Legion Knights, in the center structure on the first floor.
RC: Siegbräu - LappGiven by Lapp within the Ringed Inner Wall.
RC: Simple Gem - grave, up stairs after first dropIn Shared Grave, following the path after falling down from the crumbling stairs and continuing upward.
RC: Soul of Darkeater MidirDropped by Darkeater Midir
RC: Soul of Slave Knight GaelDropped by Slave Knight Gael
RC: Soul of a Crestfallen Knight - swamp far, behind crystalBehind a crystal structure at the far end of the muck pit, close to the building with the praying Hollow Clerics before Dragonslayer Armour.
RC: Soul of a Crestfallen Knight - swamp near left, nookIn the muck pit behind all of the Hollow Clerics near the very long ladder.
RC: Soul of a Crestfallen Knight - wall top, under dropAfter dropping down onto the side path on the right side of the Mausoleum Lookout courtyard to where the Crystal Lizard is, behind you.
RC: Soul of a Weary Warrior - lower cliff, by first alcoveIn front of the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Soul of a Weary Warrior - swamp centerIn the middle of the muck pit where the Judicator is patrolling.
RC: Soul of a Weary Warrior - swamp right, by sunken churchIn between where the Judicator patrols in the muck pit and the submerged building with the 4 Ringed Knights. Provides some shelter from his arrows.
RC: Spears of the Church - hidden boss dropDropped by Darkeater Midir
RC: Titanite Chunk - streets high, building oppositeDown a path past the room where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Titanite Chunk - streets, near left dropNear the top of the stairs by Shira, dropping down in an alcove to the left.
RC: Titanite Chunk - swamp center, peninsula edgeAlong the edge of the muck pit close to where the Judicator patrols.
RC: Titanite Chunk - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: Titanite Chunk - swamp near left, by spire topAt the edge of the muck pit, on the opposite side of the wall from the very long ladder.
RC: Titanite Chunk - swamp near right, behind rockAt the very edge of the muck pit, to the left of the submerged building with 4 Ringed Knights.
RC: Titanite Chunk - wall top, among gravesAlong the right edge of the courtyard after Mausoleum Lookout in a cluster of graves.
RC: Titanite Chunk - wall upper, courtyard alcoveIn the courtyard where the first Ringed Knight is seen, along the right wall into an alcove.
RC: Titanite Scale - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Titanite Scale - lower cliff, first alcoveIn the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Titanite Scale - lower cliff, lower pathAfter dropping down from the upper path attacked by Midir's fire-breathing to the lower path.
RC: Titanite Scale - lower cliff, path under bridgePartway down a slope to the right of the bridge where Midir first assaults you.
RC: Titanite Scale - swamp far, by minibossIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - swamp far, lagoon entranceIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - upper cliff, bridgeOn the final bridge where Midir attacks before you knock him off.
RC: Titanite Scale - wall lower, lizardDropped by the Crystal Lizard on the stairs going up from Shared Grave to Ringed Inner Wall elevator.
RC: Titanite Scale - wall top, behind spawnBehind you at the very start of the level.
RC: Titanite Slab - ashes, NPC dropGiven by Shira after defeating Midir, or dropped by her in post-Filianore Ringed City.
RC: Titanite Slab - ashes, mob dropDropped by the Ringed Knight wandering around near Gael's arena
RC: Titanite Slab - mid boss dropDropped by Halflight, Spear of the Church
RC: Twinkling Titanite - church path, left of boss doorDropping down to the left of the door leading to Halflight.
RC: Twinkling Titanite - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Twinkling Titanite - streets high, lizardDropped by the Crystal Lizard which runs across the bridge after climbing the very long ladder up from the muck pit.
RC: Twinkling Titanite - swamp near leftAt the left edge of the muck pit coming from the stairs, guarded by a Preacher Locust.
RC: Twinkling Titanite - swamp near right, on sunken churchFollowing the sloped roof of the submerged building with the 4 Ringed Knights, along the back wall
RC: Twinkling Titanite - wall top, lizard on side pathDropped by the first Crystal Lizard on the side path on the right side of the Mausoleum Lookout courtyard
RC: Twinkling Titanite - wall tower, jump from chandelierIn the cylindrical building before the long stairs with many Harald Legion Knights. Carefully drop down to the chandelier in the center, then jump to the second floor. The item is on a ledge.
RC: Violet Wrappings - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: White Birch Bow - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: White Preacher Head - swamp near, nook right of stairsPast the balcony to the right of the Ringed City Streets bonfire room entrance. Can be accessed by dropping down straight after from the bonfire, then around to the left.
RC: Wolf Ring+3 - street gardens, NPC dropDropped by Alva (invades whether embered or not, or boss defeated or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Young White Branch - swamp far left, by white tree #1Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #2Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #3Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RS: Blue Bug Pellet - broken stairs by OrbeckOn the broken stairs leading down from Orbeck's area, on the opposite side from Orbeck
RS: Blue Sentinels - HoraceGiven by Horace the Hushed by first "talking" to him, or upon death.
RS: Braille Divine Tome of Carim - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Brigand Armor - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Axe - beneath roadAt the start of the path leading down to the Madwoman in Road of Sacrifices
RS: Brigand Gauntlets - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Hood - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Trousers - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Twindaggers - beneath roadAt the end of the path guarded by the Madwoman in Road of Sacrifices
RS: Butcher Knife - NPC drop beneath roadDropped by the Butcher Knife-wielding madwoman near the start of Road of Sacrifices
RS: Chloranthy Ring+2 - road, drop across from carriageFound dropping down from the first Storyteller Corvian on the left side rather than the right side. You can then further drop down to where the madwoman is, after healing.
RS: Conjurator Boots - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Hood - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Manchettes - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Robe - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Crystal Gem - stronghold, lizardDropped by the Crystal Lizard in the building before Crystal Sage
RS: Ember - right of Halfway Fortress entranceOn the ledge with the Corvian with the Storyteller Staff, to the right of the Halfway Fortress entrance
RS: Ember - right of fire behind stronghold left roomBehind the building before Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side or go under bridge on right side
RS: Estus Shard - left of fire behind stronghold left roomBehind the building leading to Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side of go under bridge on right side
RS: Exile Greatsword - NPC drop by Farron KeepDropped by the greatsword-wielding Exile Knight before the ladder down to Farron Keep
RS: Fading Soul - woods by Crucifixion Woods bonfireDropping down from the Crucifixion Woods bonfire toward the Halfway Fortress, guarded by dogs
RS: Fallen Knight Armor - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Gauntlets - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Helm - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Trousers - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Farron Coal - keep perimeterAt the end of the Farron Keep Perimeter building on Crucifixion Woods side, behind the Black Knight
RS: Golden Falcon Shield - path from stronghold right room to Farron KeepHalfway up the stairs to the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area, go straight and follow the path down
RS: Grass Crest Shield - water by Crucifixion Woods bonfireDropping down into the crab area from Crucifixion Woods, on the other side of a tree from the greater crab
RS: Great Club - NPC drop by Farron KeepDropped by the club-wielding Exile Knight before the ladder down to Farron Keep
RS: Great Swamp Pyromancy Tome - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Great Swamp Ring - miniboss drop, by Farron KeepDropped by Greater Crab in Crucifixion Woods close to the Farron Keep outer wall
RS: Green Blossom - by deep waterIn the Crucifixion Woods crab area out in the open, close to the edge of the deep water area
RS: Green Blossom - water beneath strongholdIn the Crucifixion Woods crab area close to the Crucifixion Woods bonfire, along the left wall of the water area, to the right of the entrance to the building before Crystal Sage
RS: Heretic's Staff - stronghold left roomIn the building before Crystal Sage, entering from near Crucifixion Woods, in a corner under the first stairwell and balcony
RS: Heysel Pick - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
RS: Homeward Bone - balcony by Farron KeepAt the far end of the building where you descend into Farron Keep, by the balcony
RS: Large Soul of an Unknown Traveler - left of stairs to Farron KeepIn the area before you descend into Farron Keep, before the stairs to the far left
RS: Lingering Dragoncrest Ring+1 - waterOn a tree by the greater crab near the Crucifixion Woods bonfire, after the Grass Crest Shield tree
RS: Morne's Ring - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Ring of Sacrifice - stronghold, drop from right room balconyDrop down from the platform behind the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area
RS: Sage Ring - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sellsword Armor - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Gauntlet - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Helm - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Trousers - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Twinblades - keep perimeterIn the Farron Keep Perimeter building on Crucifixion Woods side, behind and to the right of the Black Knight
RS: Shriving Stone - road, by startDropping down to the left of the first Corvian enemy in Road of Sacrifices
RS: Sorcerer Gloves - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Hood - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Robe - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Trousers - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Soul of a Crystal SageDropped by Crystal Sage
RS: Soul of an Unknown Traveler - drop along wall from Halfway FortressFrom Halfway Fortress, hug the right wall and drop down twice on the way to the crab area
RS: Soul of an Unknown Traveler - right of door to stronghold leftOut in the open to the right of the building before Crystal Sage, as entered from Crucifixion Woods bonfire
RS: Soul of an Unknown Traveler - road, by wagonTo the right of the overturned wagon descending from the Road of Sacrifices bonfire
RS: Titanite Shard - road, on bridge after you go underCrossing the bridge you go under after the first Road of Sacrifices bonfire, after a sleeping Corvian and another Corvian guarding the pickup
RS: Titanite Shard - water by Halfway FortressDropping down into the Crucifixion Woods crab area right after Halfway Fortress, on the left wall heading toward the Black Knight building, guarded by dog
RS: Titanite Shard - woods, left of path from Halfway FortressHugging the left wall from Halfway Fortress to Crystal Sage, behind you after the first dropdown
RS: Titanite Shard - woods, surrounded by enemiesHugging the left wall from Halfway Fortress to the Crystal Sage bonfire, after a dropdown surrounded by seven Poisonhorn bugs
RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfireIn the middle of the area with the Poisonhorn bugs and Lycanthrope Hunters, following the wall where the bugs guard a Titanite Shard
RS: Xanthous Crown - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
SL: Black Iron Greatshield - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Black Knight Sword - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, guarded by a Black Knight
SL: Bloodbite Ring+1 - behind ballistaBehind the ballista, overlooking Smouldering Lake
SL: Chaos Gem - antechamber, lizard at end of long hallDropped by the Crystal Lizard found from the Antechamber bonfire, toward the Demon Cleric and to the right, then all the way down
SL: Chaos Gem - lake, far end by mobIn Smouldering Lake along the wall underneath the ballista, all the way to the left past two crabs
SL: Dragonrider Bow - by ladder from ruins basement to ballistaAfter climbing up the ladder after the Black Knight in Demon Ruins, falling back down to a ledge
SL: Ember - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Ember - ruins main lower, path to antechamberGoing down the stairs from the Antechamber bonfire, to the right, at the end of the short hallway to the next right
SL: Ember - ruins main upper, hall end by holeIn the Demon Ruins, hugging the right wall from the Demon Ruins bonfire, or making a jump from the illusory hall corridor from Antechamber bonfire
SL: Ember - ruins main upper, just after entranceBehind the first Demon Cleric from the Demon Ruins bonfire
SL: Estus Shard - antechamber, illusory wallBehind an illusory wall and Smouldering Writhing Flesh-filled corridor from Antechamber bonfire
SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, past the Black Knight, hidden in a corner
SL: Fume Ultra Greatsword - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Homeward Bone - path to ballistaIn the area targeted by the ballista after the long ladder guarded by the Black Knight, before the Bonewheel Skeletons
SL: Izalith Pyromancy Tome - antechamber, room near bonfireIn the room straight down from the Antechamber bonfire, past a Demon Cleric, surrounded by many Ghrus.
SL: Izalith Staff - ruins basement, second illusory wall behind chestPast an illusory wall to the left of the Large Hound Rat in Demon Ruins, and then past another illusory wall, before the basilisk area
SL: Knight Slayer's Ring - ruins basement, NPC dropDropped by Knight Slayer Tsorig after invading in the Catacombs
SL: Large Titanite Shard - lake, by entranceIn the middle of Smouldering Lake, close to the Abandoned Tomb
SL: Large Titanite Shard - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Large Titanite Shard - lake, by tree #1In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, by tree #2In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, straight from entranceIn the middle of Smouldering Lake, in between Abandoned Tomb and Demon Ruins
SL: Large Titanite Shard - ledge by Demon Ruins bonfireOn a corpse hanging off the ledge outside the Demon Ruins bonfire
SL: Large Titanite Shard - ruins basement, illusory wall in upper hallIn a chest past an illusory wall to the left of the Large Hound Rat in Demon Ruins, before the basilisk area
SL: Large Titanite Shard - side lake #1In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Large Titanite Shard - side lake #2In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Lightning Stake - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Llewellyn Shield - Horace dropDropped by Horace the Hushed upon death or quest completion.
SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey roomAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall
SL: Sacred Flame - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Shield of Want - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Soul of a Crestfallen Knight - ruins basement, above lavaNext to the Black Knight in Demon Ruins
SL: Soul of the Old Demon KingDropped by Old Demon King in Smouldering Lake
SL: Speckled Stoneplate Ring - lake, ballista breaks bricksBehind a destructible wall in Smouldering Lake which the ballista has to destroy
SL: Titanite Chunk - path to side lake, lizardDropped by the second Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Titanite Scale - ruins basement, path to lavaIn the area with Basilisks on the way to the ballista
SL: Toxic Mist - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, in the middle of the lava pit.
SL: Twinkling Titanite - path to side lake, lizardDropped by the first Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Undead Bone Shard - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Undead Bone Shard - ruins main lower, left after stairsIn the close end of the Demon Ruins main hall, right below a Smouldering Writhing Flesh
SL: White Hair Talisman - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, at the far end of the lava pit.
SL: Yellow Bug Pellet - side lakeIn the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
UG: Ashen Estus Ring - swamp, path opposite bonfireIn the coffin similar to your initial spawn location, guarded by Corvians
UG: Black Knight Glaive - boss arenaIn the Champion Gundyr boss area
UG: Blacksmith Hammer - shrine, Andre's roomWhere Andre sits in Firelink Shrine
UG: Chaos Blade - environs, left of shrineWhere Sword Master is in Firelink Shrine
UG: Coiled Sword Fragment - shrine, dead bonfireIn the dead Firelink Shrine bonfire
UG: Ember - shopSold by Untended Graves Handmaid
UG: Eyes of a Fire Keeper - shrine, Irina's roomBehind an illusory wall, in the same location Irina sits in Firelink Shrine
UG: Hidden Blessing - cemetery, behind coffinBehind the coffin that had a Titanite Shard in Cemetery of Ash
UG: Hornet Ring - environs, right of main path after killing FK bossOn a cliffside to the right of the main path leading up to dark Firelink Shrine, after Abyss Watchers is defeated.
UG: Life Ring+3 - shrine, behind big throneBehind Prince Lothric's throne
UG: Priestess Ring - shopSold or dropped by Untended Graves Handmaid. Killing her is not recommended
UG: Ring of Steel Protection+1 - environs, behind bell towerBehind Bell Tower to the right
UG: Shriving Stone - swamp, by bonfireAt the very start of the area
UG: Soul of Champion GundyrDropped by Champion Gundyr
UG: Soul of a Crestfallen Knight - environs, above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
UG: Soul of a Crestfallen Knight - swamp, centerClose to where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, left path by fountainIn a path to the left of where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, right path by fountainIn a path to the right of where Ashen Estus Flask was in Cemetery of Ash
UG: Wolf Knight Armor - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Gauntlets - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Helm - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Leggings - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
US: Alluring Skull - foot, behind carriageGuarded by two dogs after the Foot of the High Wall bonfire
US: Alluring Skull - on the way to tower, behind buildingAfter the ravine bridge leading to Eygon and the Giant's tower, wrapping around the building to the right.
US: Alluring Skull - tower village building, upstairsUp the stairs of the building with Cage Spiders after the Fire Demon, before the dogs
US: Bloodbite Ring - miniboss in sewerDropped by the large rat in the sewers with grave access
US: Blue Wooden Shield - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Caduceus Round Shield - right after stable exitAfter exiting the building across the bridge to the right of the first Undead Settlement building, to the left
US: Caestus - sewerIn the tunnel with the Giant Hound Rat and Grave Key door, from the ravine bridge toward Dilapidated Bridge bonfire
US: Charcoal Pine Bundle - first building, bottom floorDown the stairs in the first building
US: Charcoal Pine Bundle - first building, middle floorOn the bottom floor of the first building
US: Charcoal Pine Resin - hanging corpse roomIn the building after the burning tree and Cathedral Evangelist, in the room with the many hanging corpses
US: Chloranthy Ring - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Cleric Blue Robe - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Gloves - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Hat - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Trousers - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cornyx's Garb - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Garb - kill CornyxDropped by Cornyx
US: Cornyx's Skirt - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Skirt - kill CornyxDropped by Cornyx
US: Cornyx's Wrap - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Wrap - kill CornyxDropped by Cornyx
US: Covetous Silver Serpent Ring+2 - tower village, drop down from roofAt the back of a roof near the end of the Fire Demon loop, dropping down past where Flynn's Ring is
US: Ember - behind burning treeBehind the burning tree with the Cathedral Evangelist
US: Ember - bridge on the way to towerOn the ravine bridge leading toward Eygon and the Giant's tower
US: Ember - by stairs to bossNext to the stairs leading up to Curse-Rotted Greatwood fight, near a tree guarded by a dog
US: Ember - by white treeNear the Birch Tree where giant shoots arrows
US: Ember - tower basement, minibossIn the room with the Outrider Knight
US: Estus Shard - under burning treeIn front of the burning tree guarded by the Cathedral Evangelist
US: Fading Soul - by white treeNear the Birch Tree where giant shoots arrows
US: Fading Soul - outside stableIn the thrall area to the right of the bridge to the right of the burning tree with the Cathedral Evangelist
US: Fire Clutch Ring - wooden walkway past stableFrom the area bombarded by firebombs above the Cliff Underside bonfire
US: Fire Gem - tower village, miniboss dropDropped by the Fire Demon you fight with Siegward
US: Firebomb - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a rooftop overlooking the Cliff Underside area.
US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transportOn a hanging corpse in the area with the Pit of Hollows cage manservant, after the thrall area, overlooking the entrance to the Giant's tower.
US: Flynn's Ring - tower village, rooftopOn the roof toward the end of the Fire Demon loop, past the Cathedral Evangelists
US: Great Scythe - building by white tree, balconyOn the balcony of the building before Curse-Rotted Greatwood, coming from Dilapidated Bridge bonfire
US: Hand Axe - by CornyxNext to Cornyx's cell
US: Hawk Ring - Giant ArcherDropped by Giant, either by killing him or collecting all of the birch tree items locations in the base game.
US: Heavy Gem - HawkwoodGiven or dropped by Hawkwood after defeating Curse-Rotted Greatwood or Crystal Sage
US: Heavy Gem - chasm, lizardDrop by Crystal Lizard in ravine accessible by Grave Key or dropping down near Eygon.
US: Homeward Bone - foot, drop overlookUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Homeward Bone - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a roof overlooking the ravine bridge.
US: Homeward Bone - tower village, jump from roofAt the end of the loop from the Siegward Demon fight, after dropping down from the roof onto the tower with Chloranthy Ring, to the right of the tower entrance
US: Homeward Bone - tower village, right at startUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Human Pine Resin - tower village building, chest upstairsIn a chest after Fire Demon. Cage Spiders activate open opening it.
US: Irithyll Straight Sword - miniboss drop, by Road of SacrificesDropped by the Boreal Outright Knight before Road of Sacrifices
US: Kukri - hanging corpse above burning treeHanging corpse high above the burning tree with the Cathedral Evangelist. Must be shot down with an arrow or projective.
US: Large Club - tower village, by minibossIn the Fire Demon area
US: Large Soul of a Deserted Corpse - across from Foot of the High WallOn the opposite tower from the Foot of the High Wall bonfire
US: Large Soul of a Deserted Corpse - around corner by Cliff UndersideAfter going up the stairs from Curse-Rotted Greatwood to Cliff Underside area, on a cliff edge to the right
US: Large Soul of a Deserted Corpse - by white treeNear the Birch Tree where giant shoots arrows
US: Large Soul of a Deserted Corpse - hanging corpse room, over stairsOn a hanging corpse in the building after the burning tree. Can be knocked down by dropping onto the stairs through the broken railing.
US: Large Soul of a Deserted Corpse - on the way to tower, by wellAfter the ravine bridge leading toward Eygon and the Giant's tower, next to the well to the right
US: Large Soul of a Deserted Corpse - stableIn the building with stables across the bridge and to the right from the first Undead Settlement building
US: Life Ring+1 - tower on the way to villageOn the wooden rafters near where Siegward is waiting for Fire Demon
US: Loincloth - by Velka statueNext to the Velka statue. Requires Grave Key or dropping down near Eygon and backtracking through the skeleton area.
US: Loretta's Bone - first building, hanging corpse on balconyOn a hanging corpse after the first building, can be knocked down by rolling into it
US: Mirrah Gloves - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Trousers - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Vest - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mortician's Ashes - graveyard by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, at the close end of the graveyard
US: Mound-makers - HodrickGiven by Hodrick if accessing the Pit of Hollows before fighting Curse-Rotted Greatwood, or dropped after invading him with Sirris.
US: Northern Armor - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Gloves - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Helm - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Trousers - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Old Sage's Blindfold - kill CornyxDropped by Cornyx
US: Pale Tongue - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Partizan - hanging corpse above Cliff UndersideOn a hanging corpse on the path from Cliff Underside to Cornyx's cage. Must be shot down with an arrow or projective.
US: Plank Shield - outside stable, by NPCIn the thrall area across the bridge from the first Undead Settlement building, on a cliff edge overlooking the ravine bridge.
US: Poisonbite Ring+1 - graveyard by white tree, near wellBehind the well in the back of area where the Giant shoots arrows, nearby where the flamberge-wielding thrall drops down.
US: Pyromancy Flame - CornyxGiven by Cornyx in Firelink Shrine or dropped.
US: Red Bug Pellet - tower village building, basementOn the floor of the building after the Fire Demon encounter
US: Red Hilted Halberd - chasm cryptIn the skeleton area accessible from Grave Key or dropping down from near Eygon
US: Red and White Shield - chasm, hanging corpseOn a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.
US: Reinforced Club - by white treeNear the Birch Tree where giant shoots arrows
US: Repair Powder - first building, balconyOn the balcony of the first Undead Settlement building
US: Rusted Coin - awning above Dilapidated BridgeOn a wooden ledge near the Dilapidated Bridge bonfire. Must be jumped to from near Cathedral Evangelist enemy
US: Saint's Talisman - chasm, by ladderFrom the ravine accessible via Grave Key or dropping near Eygon, before ladder leading up to Irina of Carim
US: Sharp Gem - lizard by Dilapidated BridgeDrop by Crystal Lizard near Dilapidated Bridge bonfire.
US: Siegbräu - SiegwardGiven by Siegward after helping him defeat the Fire Demon.
US: Small Leather Shield - first building, hanging corpse by entranceHanging corpse in the first building, to the right of the entrance
US: Soul of a Nameless Soldier - top of towerAt the top of the tower where Giant shoots arrows
US: Soul of an Unknown Traveler - back alley, past cratesAfter exiting the building after the burning tree on the way to the Dilapidated Bridge bonfire. Hidden behind some crates between two buildings on the right.
US: Soul of an Unknown Traveler - chasm cryptIn the skeleton area accessible Grave Key or dropping down from near Eygon
US: Soul of an Unknown Traveler - pillory past stableIn the area bombarded by firebombs above the Cliff Underside bonfire
US: Soul of an Unknown Traveler - portcullis by burning treeBehind a grate to the left of the burning tree and Cathedral Evangelist
US: Soul of the Rotted GreatwoodDropped by Curse Rotted Greatwood
US: Spotted Whip - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Sunset Armor - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Gauntlets - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Helm - Pit of Hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Leggings - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Titanite Shard - back alley, side pathOn a side path to the right of the Cathedral Evangelist before the Dilapidated Bridge bonfire
US: Titanite Shard - back alley, up ladderNext to the Cathedral Evangelist close to the Dilapidated Bridge bonfire
US: Titanite Shard - chasm #1In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - chasm #2In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - lower path to Cliff UndersideAt the end of the cliffside path next to Cliff Underside bonfire, guarded by a Hollow Peasant wielding a four-pronged plow.
US: Titanite Shard - porch after burning treeIn front of the building after the burning tree and Cathedral Evangelist
US: Tower Key - kill IrinaDropped by Irina of Carim
US: Transposing Kiln - boss dropDropped by Curse Rotted Greatwood
US: Undead Bone Shard - by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, jumping to the floating platform on the right
US: Wargod Wooden Shield - Pit of HollowsIn the Pit of Hollows
US: Warrior of Sunlight - hanging corpse room, drop through holeDropping through a hole in the floor in the first building after the burning tree.
US: Whip - back alley, behind wooden wallIn one of the houses between building after the burning tree and the Dilapidated Bridge bonfire
US: Young White Branch - by white tree #1Near the Birch Tree where giant shoots arrows
US: Young White Branch - by white tree #2Near the Birch Tree where giant shoots arrows
+ diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 61215dbc6043..ed90289a8baf 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -7,48 +7,49 @@ ## Optional Software -- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases) +- Map tracker not yet updated for 3.0.0 -## General Concept +## Setting Up - -**This mod can ban you permanently from the FromSoftware servers if used online.** - -The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command -prompt where you can read information about your run and write any command to interact with the Archipelago server. +First, download the client from the link above. It doesn't need to go into any particular directory; +it'll automatically locate _Dark Souls III_ in your Steam installation folder. -This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch. +Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This +is the latest version, so you don't need to do any downpatching! However, if you've already +downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest +version before using this version. -## Downpatching Dark Souls III +### One-Time Setup -To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database. +Before you first connect to a multiworld, you need to generate the local data files for your world's +randomized item and (optionally) enemy locations. You only need to do this once per multiworld. -1. Launch Steam (in online mode). -2. Press the Windows Key + R. This will open the Run window. -3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode. -4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`. -5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background. -6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`. -7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`. -8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`. -9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\\AppData\Roaming\DarkSoulsIII\`. -10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III. +1. Before you first connect to a multiworld, run `randomizer\DS3Randomizer.exe`. +2. Put in your Archipelago room address (usually something like `archipelago.gg:12345`), your player + name (also known as your "slot name"), and your password if you have one. -## Installing the Archipelago mod +3. Click "Load" and wait a minute or two. -Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and -add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`) +### Running and Connecting the Game -## Joining a MultiWorld Game +To run _Dark Souls III_ in Archipelago mode: -1. Run Steam in offline mode to avoid being banned. -2. Launch Dark Souls III. -3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one. -4. Once connected, create a new game, choose a class and wait for the others before starting. -5. You can quit and launch at anytime during a game. +1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the + DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. -## Where do I get a config file? +2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that + you can use to interact with the Archipelago server. + +3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the + appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`. + +4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have + control of your character and the connection is established. + +## Frequently Asked Questions + +### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to configure your personal options and export them into a config file. diff --git a/worlds/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py new file mode 100644 index 000000000000..e590cd732b41 --- /dev/null +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -0,0 +1,27 @@ +from test.TestBase import WorldTestBase + +from worlds.dark_souls_3.Items import item_dictionary +from worlds.dark_souls_3.Locations import location_tables +from worlds.dark_souls_3.Bosses import all_bosses + +class DarkSouls3Test(WorldTestBase): + game = "Dark Souls III" + + def testLocationDefaultItems(self): + for locations in location_tables.values(): + for location in locations: + if location.default_item_name: + self.assertIn(location.default_item_name, item_dictionary) + + def testLocationsUnique(self): + names = set() + for locations in location_tables.values(): + for location in locations: + self.assertNotIn(location.name, names) + names.add(location.name) + + def testBossLocations(self): + all_locations = {location.name for locations in location_tables.values() for location in locations} + for boss in all_bosses: + for location in boss.locations: + self.assertIn(location, all_locations) diff --git a/worlds/dark_souls_3/test/__init__.py b/worlds/dark_souls_3/test/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 From 6e41c6067208d0286f56694f4281d03a0aaf6dad Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 13:13:01 +0100 Subject: [PATCH 127/393] Core: Check parent_region.can_reach first in Location.can_reach (#3724) * Core: Check parent_region.can_reach first in Location.can_reach The comment about self.access_rule computing faster on average appears to no longer be correct with the current caching system for region accessibility, resulting in self.parent_region.can_reach computing faster on average. Generation of template yamls for each game that does not require a rom to generate, generated with `python -O .\Generate.py --seed 1` (all durations averaged over at 4 or 5 generations): Full generation with `spoiler: 1` and no progression balancing: 89.9s -> 72.6s Only output from above case: 2.6s -> 2.2s Full generation with `spoiler: 3` and no progression balancing: 769.9s -> 627.1s Only playthrough calculation + paths from above case: 680.5s -> 555.3s Full generation with `spoiler: 1` with default progression balancing: 123.5s -> 98.3s Only progression balancing from above case: 11.3s -> 9.6s * Update BaseClasses.py * Update BaseClasses.py * Update BaseClasses.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 81601506d084..34e7248415f6 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1128,9 +1128,9 @@ def can_fill(self, state: CollectionState, item: Item, check_access=True) -> boo and (not check_access or self.can_reach(state)))) def can_reach(self, state: CollectionState) -> bool: - # self.access_rule computes faster on average, so placing it first for faster abort + # Region.can_reach is just a cache lookup, so placing it first for faster abort on average assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and self.parent_region.can_reach(state) + return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): if self.item: From 30f97dd7dead5cad3a9d7a1eecf3737a210098c7 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 13:25:39 +0100 Subject: [PATCH 128/393] Core: Speed up CollectionState.copy() using built-in copy methods (#3678) All the types being copied are built-in types with their own `copy()` methods, so using the `copy` module was a bit overkill and also slower. This patch replaces the use of the `copy` module in `CollectionState.copy()` with using the built-in `.copy()` methods. The copying of `reachable_regions` and `blocked_connections` was also iterating the keys of each dictionary and then looking up the value in the dictionary for that key. It is faster, and I think more readable, to iterate the dictionary's `.items()` instead. For me, when generating a multiworld including the template yaml of every world with `python -O .\Generate.py --skip_output`, this patch saves about 2.1s. The overall generation duration for these yamls varies quite a lot, but averages around 160s for me, so on average this patch reduced overall generation duration (excluding output duration) by around 1.3%. Timing comparisons were made by calling time.perf_counter() at the start and end of `CollectionState.copy()`'s body, and summing the differences between the starts and ends of the method body into a global variable that was printed at the end of generation. Additional timing comparisons were made, using the `timeit` module, of the individual function calls or dictionary comprehensions used to perform the copying. The main performance cost was `copy.deepcopy()`, which gets slow as the number of keys multiplied by the number of values within the sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]` with 100 keys and where each Counter contains 100 keys was 30x slower than most other tested copying methods. Increasing the number of dict keys or Counter keys only makes it slower. --- BaseClasses.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 34e7248415f6..092f330bcbb4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,7 +1,6 @@ from __future__ import annotations import collections -import copy import itertools import functools import logging @@ -719,14 +718,14 @@ def update_reachable_regions(self, player: int): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = copy.deepcopy(self.prog_items) - ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in - self.reachable_regions} - ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in - self.blocked_connections} - ret.events = copy.copy(self.events) - ret.path = copy.copy(self.path) - ret.locations_checked = copy.copy(self.locations_checked) + ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} + ret.reachable_regions = {player: region_set.copy() for player, region_set in + self.reachable_regions.items()} + ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in + self.blocked_connections.items()} + ret.events = self.events.copy() + ret.path = self.path.copy() + ret.locations_checked = self.locations_checked.copy() for function in self.additional_copy_functions: ret = function(self, ret) return ret From ac7590e621be1662e9522022670c5949e0195cfa Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 16:02:41 +0100 Subject: [PATCH 129/393] HK: fix iterating all worlds instead of only HK worlds in stage_pre_fill (#3750) Would cause generation to fail when generating with HK and another game. Mistake in 6803c373e5ff. --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 99277378a162..cbb909606127 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -467,7 +467,7 @@ def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] if worlds: grubs = [item for item in multiworld.get_items() if item.name == "Grub"] - all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] if all_grub_players: group_lookup = defaultdict(set) From c66a8605da1ba1aaaaac57bc2722ee1b585d4c5d Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Fri, 9 Aug 2024 08:04:59 -0700 Subject: [PATCH 130/393] DOOM, DOOM II: Update steam URLs (#3746) --- worlds/doom_1993/docs/setup_en.md | 2 +- worlds/doom_ii/docs/setup_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index 8906efac9cea..5d96e6a8056e 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM_1993/) +- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md index 87054ab30783..ec6697c76da2 100644 --- a/worlds/doom_ii/docs/setup_en.md +++ b/worlds/doom_ii/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software From a6f376b02e48e98f253b42ca66fb99eaa927a1ab Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:38:42 -0400 Subject: [PATCH 131/393] TLOZ: world: multiworld (#3752) --- worlds/tloz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 8ea5f3e18ca1..c8c76bd85a8a 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -110,8 +110,8 @@ class TLoZWorld(World): if v is not None: location_name_to_id[k] = v + base_id - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.generator_in_use = threading.Event() self.rom_name_available_event = threading.Event() self.levels = None From 9dba39b6064b162124885f556b7b72476774907e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:08:24 +0200 Subject: [PATCH 132/393] SoE: fix determinism (#3745) Fixes randomly placed ingredients not being deterministic (depending on settings) and in turn also fixes logic not being deterministic if they get replaced by fragments. --- worlds/soe/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 3baed165d821..161c749fd6bd 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -188,6 +188,7 @@ class SoEWorld(World): connect_name: str _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] + _fillers = sorted(item_name_groups["Ingredients"]) def __init__(self, multiworld: "MultiWorld", player: int): self.connect_name_available_event = threading.Event() @@ -469,7 +470,7 @@ def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None: multidata["connect_names"][self.connect_name] = payload def get_filler_item_name(self) -> str: - return self.random.choice(list(self.item_name_groups["Ingredients"])) + return self.random.choice(self._fillers) class SoEItem(Item): From 8e06ab4f688c5e32350bea51ae47f8fcb3dd3e71 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 10 Aug 2024 06:49:32 -0500 Subject: [PATCH 133/393] Core: fix invalid __package__ of zipped worlds (#3686) * fix invalid package fix * add comment describing fix --- worlds/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/__init__.py b/worlds/__init__.py index bb2fe866d02d..c277ac9ca1de 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -73,7 +73,12 @@ def load(self) -> bool: else: # TODO: remove with 3.8 support mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) - mod.__package__ = f"worlds.{mod.__package__}" + if mod.__package__ is not None: + mod.__package__ = f"worlds.{mod.__package__}" + else: + # load_module does not populate package, we'll have to assume mod.__name__ is correct here + # probably safe to remove with 3.8 support + mod.__package__ = f"worlds.{mod.__name__}" mod.__name__ = f"worlds.{mod.__name__}" sys.modules[mod.__name__] = mod with warnings.catch_warnings(): From 68a92b0c6fe5b011da20ab5338c3f23757f1876d Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 11 Aug 2024 08:47:17 -0400 Subject: [PATCH 134/393] Clique: Update to new options API (#3759) --- worlds/clique/Items.py | 13 ++++++++----- worlds/clique/Locations.py | 11 +++++++---- worlds/clique/Options.py | 16 ++++++++-------- worlds/clique/Rules.py | 13 ++++++++----- worlds/clique/__init__.py | 32 ++++++++++++++++---------------- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/worlds/clique/Items.py b/worlds/clique/Items.py index 5474f58b82d5..81e2540bacc0 100644 --- a/worlds/clique/Items.py +++ b/worlds/clique/Items.py @@ -1,6 +1,9 @@ -from typing import Callable, Dict, NamedTuple, Optional +from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import Item, ItemClassification, MultiWorld +from BaseClasses import Item, ItemClassification + +if TYPE_CHECKING: + from . import CliqueWorld class CliqueItem(Item): @@ -10,7 +13,7 @@ class CliqueItem(Item): class CliqueItemData(NamedTuple): code: Optional[int] = None type: ItemClassification = ItemClassification.filler - can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True + can_create: Callable[["CliqueWorld"], bool] = lambda world: True item_data_table: Dict[str, CliqueItemData] = { @@ -21,11 +24,11 @@ class CliqueItemData(NamedTuple): "Button Activation": CliqueItemData( code=69696968, type=ItemClassification.progression, - can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), + can_create=lambda world: world.options.hard_mode, ), "A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData( code=69696967, - can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`. + can_create=lambda world: False # Only created from `get_filler_item_name`. ), "The Urge to Push": CliqueItemData( type=ItemClassification.progression, diff --git a/worlds/clique/Locations.py b/worlds/clique/Locations.py index 144becae5368..900b497eb4eb 100644 --- a/worlds/clique/Locations.py +++ b/worlds/clique/Locations.py @@ -1,6 +1,9 @@ -from typing import Callable, Dict, NamedTuple, Optional +from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import Location, MultiWorld +from BaseClasses import Location + +if TYPE_CHECKING: + from . import CliqueWorld class CliqueLocation(Location): @@ -10,7 +13,7 @@ class CliqueLocation(Location): class CliqueLocationData(NamedTuple): region: str address: Optional[int] = None - can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True + can_create: Callable[["CliqueWorld"], bool] = lambda world: True locked_item: Optional[str] = None @@ -22,7 +25,7 @@ class CliqueLocationData(NamedTuple): "The Item on the Desk": CliqueLocationData( region="The Button Realm", address=69696968, - can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), + can_create=lambda world: world.options.hard_mode, ), "In the Player's Mind": CliqueLocationData( region="The Button Realm", diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py index 7976dcb62130..d88a1289903c 100644 --- a/worlds/clique/Options.py +++ b/worlds/clique/Options.py @@ -1,6 +1,5 @@ -from typing import Dict - -from Options import Choice, Option, Toggle +from dataclasses import dataclass +from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool class HardMode(Toggle): @@ -25,10 +24,11 @@ class ButtonColor(Choice): option_black = 11 -clique_options: Dict[str, type(Option)] = { - "color": ButtonColor, - "hard_mode": HardMode, +@dataclass +class CliqueOptions(PerGameCommonOptions): + color: ButtonColor + hard_mode: HardMode + start_inventory_from_pool: StartInventoryPool # DeathLink is always on. Always. - # "death_link": DeathLink, -} + # death_link: DeathLink diff --git a/worlds/clique/Rules.py b/worlds/clique/Rules.py index 5ae1d2c68e39..63ecd4e9e17c 100644 --- a/worlds/clique/Rules.py +++ b/worlds/clique/Rules.py @@ -1,10 +1,13 @@ -from typing import Callable +from typing import Callable, TYPE_CHECKING -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +if TYPE_CHECKING: + from . import CliqueWorld -def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]: - if getattr(multiworld, "hard_mode")[player]: - return lambda state: state.has("Button Activation", player) + +def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]: + if world.options.hard_mode: + return lambda state: state.has("Button Activation", world.player) return lambda state: True diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index b5cc74d94ac0..3d06e477eba7 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -1,10 +1,10 @@ -from typing import List +from typing import List, Dict, Any from BaseClasses import Region, Tutorial from worlds.AutoWorld import WebWorld, World from .Items import CliqueItem, item_data_table, item_table from .Locations import CliqueLocation, location_data_table, location_table, locked_locations -from .Options import clique_options +from .Options import CliqueOptions from .Regions import region_data_table from .Rules import get_button_rule @@ -38,7 +38,8 @@ class CliqueWorld(World): game = "Clique" web = CliqueWebWorld() - option_definitions = clique_options + options: CliqueOptions + options_dataclass = CliqueOptions location_name_to_id = location_table item_name_to_id = item_table @@ -48,7 +49,7 @@ def create_item(self, name: str) -> CliqueItem: def create_items(self) -> None: item_pool: List[CliqueItem] = [] for name, item in item_data_table.items(): - if item.code and item.can_create(self.multiworld, self.player): + if item.code and item.can_create(self): item_pool.append(self.create_item(name)) self.multiworld.itempool += item_pool @@ -61,41 +62,40 @@ def create_regions(self) -> None: # Create locations. for region_name, region_data in region_data_table.items(): - region = self.multiworld.get_region(region_name, self.player) + region = self.get_region(region_name) region.add_locations({ location_name: location_data.address for location_name, location_data in location_data_table.items() - if location_data.region == region_name and location_data.can_create(self.multiworld, self.player) + if location_data.region == region_name and location_data.can_create(self) }, CliqueLocation) region.add_exits(region_data_table[region_name].connecting_regions) # Place locked locations. for location_name, location_data in locked_locations.items(): # Ignore locations we never created. - if not location_data.can_create(self.multiworld, self.player): + if not location_data.can_create(self): continue locked_item = self.create_item(location_data_table[location_name].locked_item) - self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item) + self.get_location(location_name).place_locked_item(locked_item) # Set priority location for the Big Red Button! - self.multiworld.priority_locations[self.player].value.add("The Big Red Button") + self.options.priority_locations.value.add("The Big Red Button") def get_filler_item_name(self) -> str: return "A Cool Filler Item (No Satisfaction Guaranteed)" def set_rules(self) -> None: - button_rule = get_button_rule(self.multiworld, self.player) - self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule - self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule + button_rule = get_button_rule(self) + self.get_location("The Big Red Button").access_rule = button_rule + self.get_location("In the Player's Mind").access_rule = button_rule # Do not allow button activations on buttons. - self.multiworld.get_location("The Big Red Button", self.player).item_rule =\ - lambda item: item.name != "Button Activation" + self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation" # Completion condition. self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player) - def fill_slot_data(self): + def fill_slot_data(self) -> Dict[str, Any]: return { - "color": getattr(self.multiworld, "color")[self.player].current_key + "color": self.options.color.current_key } From 09e052c750fcd1fa2e56afb8e9ec39e95775e382 Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 12 Aug 2024 00:24:09 +0200 Subject: [PATCH 135/393] Timespinner: Fix eels check logic #3777 --- worlds/timespinner/Locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 86839f0f2167..f99dd7615571 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -135,11 +135,11 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Upper Lake Serene', 'Lake Serene: Pyramid keys room', 1337104), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Chicken ledge', 1337174), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Deep dive', 1337105), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Under the eels', 1337106, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Past the eels', 1337110, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), From 21bbf5fb95bcf6c1cc842197ca44ec253c5daeb4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 11 Aug 2024 18:24:30 -0400 Subject: [PATCH 136/393] TUNIC: Add note to Universal Tracker stuff #3772 --- worlds/tunic/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 5253e9951437..6657b464ed2d 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -410,7 +410,9 @@ def fill_slot_data(self) -> Dict[str, Any]: return slot_data # for the universal tracker, doesn't get called in standard gen + # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md @staticmethod def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough + # we are using re_gen_passthrough over modifying the world here due to complexities with ER return slot_data From ae0abd38217d03bba2f2ab78ee6a2311c5fdb995 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 11 Aug 2024 17:57:59 -0500 Subject: [PATCH 137/393] Core: change start inventory from pool to warn when nothing to remove (#3158) * makes start inventory from pool warn and fixes the itempool to match when it can not find a matching item to remove * calc the difference correctly * save new filler and non-removed items differently so we don't remove existing items at random --- Main.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index ce054dcd393f..6dc03aaa55e0 100644 --- a/Main.py +++ b/Main.py @@ -151,6 +151,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # Because some worlds don't actually create items during create_items this has to be as late as possible. if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): new_items: List[Item] = [] + old_items: List[Item] = [] depletion_pool: Dict[int, Dict[str, int]] = { player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", @@ -169,20 +170,24 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No depletion_pool[item.player][item.name] -= 1 # quick abort if we have found all items if not target: - new_items.extend(multiworld.itempool[i+1:]) + old_items.extend(multiworld.itempool[i+1:]) break else: - new_items.append(item) + old_items.append(item) # leftovers? if target: for player, remaining_items in depletion_pool.items(): remaining_items = {name: count for name, count in remaining_items.items() if count} if remaining_items: - raise Exception(f"{multiworld.get_player_name(player)}" + logger.warning(f"{multiworld.get_player_name(player)}" f" is trying to remove items from their pool that don't exist: {remaining_items}") - assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items + # find all filler we generated for the current player and remove until it matches + removables = [item for item in new_items if item.player == player] + for _ in range(sum(remaining_items.values())): + new_items.remove(removables.pop()) + assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change." + multiworld.itempool[:] = new_items + old_items multiworld.link_items() From a3e54a951fb2ea322fe5a13ba09024e57425a4a2 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:53:40 -0400 Subject: [PATCH 138/393] Undertale: Fix slot_data and options.as_dict() (#3774) * Undertale: Fixing slot_data * Booleans were difficult --- worlds/undertale/__init__.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 9084c77b0065..9f09bb34526b 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -67,12 +67,15 @@ def _get_undertale_data(self): "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), + "key_pieces": int(self.options.key_pieces.value), + "rando_love": bool(self.options.rando_love and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), + "rando_stats": bool(self.options.rando_stats and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), "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) + "rando_item_button": bool(self.options.rando_item_button.value), + "route_required": int(self.options.route_required.value), + "temy_include": int(self.options.temy_include.value) + } def get_filler_item_name(self): @@ -220,16 +223,7 @@ def UndertaleRegion(region_name: str, exits=[]): link_undertale_areas(self.multiworld, self.player) def fill_slot_data(self): - slot_data = self._get_undertale_data() - 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.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) - return slot_data + return self._get_undertale_data() def create_item(self, name: str) -> Item: item_data = item_table[name] From 67520adceae7d7a1222afd319ab274868f3bd3b9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 11 Aug 2024 20:13:45 -0400 Subject: [PATCH 139/393] Core: Error on empty options.as_dict (#3773) * Error on empty options.as_dict * ValueError instead * Apply suggestions from code review Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Options.py b/Options.py index d040828509d1..e22ee1ee63a0 100644 --- a/Options.py +++ b/Options.py @@ -1236,6 +1236,7 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` """ + assert option_names, "options.as_dict() was used without any option names." option_results = {} for option_name in option_names: if option_name in type(self).type_hints: From 50330cf32f05e5e1afb65b51f4fd5c01391c3534 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:32:14 +0200 Subject: [PATCH 140/393] Core: Remove broken unused code from Options.py (#3781) "Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained --- Options.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/Options.py b/Options.py index e22ee1ee63a0..ecde6275f1ea 100644 --- a/Options.py +++ b/Options.py @@ -1518,31 +1518,3 @@ def yaml_dump_scalar(scalar) -> str: with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) - - -if __name__ == "__main__": - - from worlds.alttp.Options import Logic - import argparse - - map_shuffle = Toggle - compass_shuffle = Toggle - key_shuffle = Toggle - big_key_shuffle = Toggle - hints = Toggle - test = argparse.Namespace() - test.logic = Logic.from_text("no_logic") - test.map_shuffle = map_shuffle.from_text("ON") - test.hints = hints.from_text('OFF') - try: - test.logic = Logic.from_text("overworld_glitches_typo") - except KeyError as e: - print(e) - try: - test.logic_owg = Logic.from_text("owg") - except KeyError as e: - print(e) - if test.map_shuffle: - print("map_shuffle is on") - print(f"Hints are {bool(test.hints)}") - print(test) From dcaa2f7b971d9b41de7a84331008a095223ac997 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:02:09 -0400 Subject: [PATCH 141/393] Core: Two Small Fixes (#3782) --- BaseClasses.py | 2 +- Launcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 092f330bcbb4..97e792cc5cc2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1427,7 +1427,7 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st # Maybe move the big bomb over to the Event system instead? if any(exit_path == 'Pyramid Fairy' for path in self.paths.values() for (_, exit_path) in path): - if multiworld.mode[player] != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Big Bomb Shop', player)) else: diff --git a/Launcher.py b/Launcher.py index e4b65be93a68..6b66b2a3a671 100644 --- a/Launcher.py +++ b/Launcher.py @@ -266,7 +266,7 @@ def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None if file and component: run_component(component, file) else: - logging.warning(f"unable to identify component for {filename}") + logging.warning(f"unable to identify component for {file}") def _stop(self, *largs): # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. From 96d48a923a34a1cd4c5f64a5f2e12acc657dd041 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 15:28:05 -0500 Subject: [PATCH 142/393] Core: recontextualize `CollectionState.collect` (#3723) * Core: renamed `CollectionState.collect` arg from `event` to `prevent_sweep` and remove forced collection * Update TestDungeon.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 8 +-- test/bases.py | 2 +- worlds/alttp/test/dungeons/TestDungeon.py | 4 +- worlds/oot/EntranceShuffle.py | 2 +- worlds/oot/__init__.py | 2 +- worlds/stardew_valley/test/TestCrops.py | 8 +-- .../stardew_valley/test/TestDynamicGoals.py | 34 ++++++------ worlds/stardew_valley/test/TestLogic.py | 2 +- worlds/stardew_valley/test/__init__.py | 8 +-- .../test/assertion/world_assert.py | 4 +- .../stardew_valley/test/rules/TestArcades.py | 52 +++++++++---------- .../test/rules/TestBuildings.py | 14 ++--- .../test/rules/TestCookingRecipes.py | 32 ++++++------ .../test/rules/TestCraftingRecipes.py | 26 +++++----- .../test/rules/TestDonations.py | 6 +-- .../test/rules/TestFriendship.py | 34 ++++++------ .../stardew_valley/test/rules/TestShipping.py | 2 +- worlds/stardew_valley/test/rules/TestTools.py | 40 +++++++------- .../stardew_valley/test/rules/TestWeapons.py | 16 +++--- 19 files changed, 146 insertions(+), 150 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 97e792cc5cc2..95f24af26548 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -863,19 +863,15 @@ def count_group_unique(self, item_name_group: str, player: int) -> int: ) # Item related - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: + def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) changed = self.multiworld.worlds[item.player].collect(self, item) - if not changed and event: - self.prog_items[item.player][item.name] += 1 - changed = True - self.stale[item.player] = True - if changed and not event: + if changed and not prevent_sweep: self.sweep_for_events() return changed diff --git a/test/bases.py b/test/bases.py index 5c2d241cbbfe..9fb223af2ac1 100644 --- a/test/bases.py +++ b/test/bases.py @@ -23,7 +23,7 @@ def get_state(self, items): state = CollectionState(self.multiworld) for item in items: item.classification = ItemClassification.progression - state.collect(item, event=True) + state.collect(item, prevent_sweep=True) state.sweep_for_events() state.update_reachable_regions(1) self._state_cache[self.multiworld, tuple(items)] = state diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 91fc462c4ecc..796bfeec3f0e 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -54,7 +54,7 @@ def run_tests(self, access_pool): for item in items: item.classification = ItemClassification.progression - state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up + state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_events() and picking up state.sweep_for_events() # key drop keys repeatedly - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") \ No newline at end of file + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index bbdc30490c18..058fdbed0011 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -796,7 +796,7 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() - time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) + time_travel_state.collect(ootworld.create_item('Time Travel'), prevent_sweep=True) time_travel_state._oot_update_age_reachable_regions(player) # Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 89f10a5a2da0..24be303f822b 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1388,7 +1388,7 @@ def get_state_with_complete_itempool(self): self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: - all_state.collect(self.create_item("Scarecrow Song"), event=True) + all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True) all_state.stale[self.player] = True return all_state diff --git a/worlds/stardew_valley/test/TestCrops.py b/worlds/stardew_valley/test/TestCrops.py index 38b736367b80..362e6bf27e7c 100644 --- a/worlds/stardew_valley/test/TestCrops.py +++ b/worlds/stardew_valley/test/TestCrops.py @@ -11,10 +11,10 @@ def test_need_greenhouse_for_cactus(self): harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit") self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), event=False) + self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) + self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), prevent_sweep=False) self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Greenhouse"), event=False) + self.multiworld.state.collect(self.world.create_item("Greenhouse"), prevent_sweep=False) self.assert_rule_true(harvest_cactus, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py index fe1bfb5f3044..bfa58dd34063 100644 --- a/worlds/stardew_valley/test/TestDynamicGoals.py +++ b/worlds/stardew_valley/test/TestDynamicGoals.py @@ -12,29 +12,29 @@ def collect_fishing_abilities(tester: SVTestBase): for i in range(4): - tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.axe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), event=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.axe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), prevent_sweep=False) for i in range(10): - tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Combat Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Mining Level"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Combat Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Mining Level"), prevent_sweep=False) for i in range(17): - tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Spring"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Summer"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Fall"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Winter"), event=False) - tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), event=False) - tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Spring"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Summer"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False) def create_and_collect(tester: SVTestBase, item_name: str) -> StardewItem: item = tester.world.create_item(item_name) - tester.multiworld.state.collect(item, event=False) + tester.multiworld.state.collect(item, prevent_sweep=False) return item diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 65f7352a5e36..da00a0f43e43 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -12,7 +12,7 @@ def collect_all(mw): for item in mw.get_items(): - mw.state.collect(item, event=True) + mw.state.collect(item, prevent_sweep=True) class LogicTestBase(RuleAssertMixin, TestCase): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 7e82ea91e434..c2c2a6a20baf 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -257,16 +257,16 @@ def run_default_tests(self) -> bool: return super().run_default_tests def collect_lots_of_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) 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) + self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) def collect_all_the_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) 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) + self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) def collect_everything(self): non_event_items = [item for item in self.multiworld.get_items() if item.code] diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index c1c24bdf75b4..97172834543c 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -33,14 +33,14 @@ def assert_item_was_necessary_for_victory(self, item: StardewItem, multiworld: M self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_cannot_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_item_was_not_necessary_for_victory(self, item: StardewItem, multiworld: MultiWorld): self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_can_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_can_win(self, multiworld: MultiWorld): diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py index fb62a456378a..2922ecfb5d9e 100644 --- a/worlds/stardew_valley/test/rules/TestArcades.py +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -19,8 +19,8 @@ def test_prairie_king(self): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=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)) @@ -28,8 +28,8 @@ def test_prairie_king(self): self.remove(boots) self.remove(gun) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(boots, prevent_sweep=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)) @@ -37,10 +37,10 @@ def test_prairie_king(self): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=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)) @@ -50,13 +50,13 @@ def test_prairie_king(self): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=True) + self.multiworld.state.collect(drop, prevent_sweep=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)) @@ -69,17 +69,17 @@ def test_prairie_king(self): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=True) + self.multiworld.state.collect(drop, prevent_sweep=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)) diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py index b00e4138a195..2c276d8b5cbe 100644 --- a/worlds/stardew_valley/test/rules/TestBuildings.py +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -23,11 +23,11 @@ def test_big_coop_blueprint(self): 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=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)}") @@ -35,13 +35,13 @@ 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True) self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) def test_big_shed_blueprint(self): @@ -53,10 +53,10 @@ def test_big_shed_blueprint(self): 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Shed"), prevent_sweep=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/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py index 81a91d1e7482..7ab9d61cb942 100644 --- a/worlds/stardew_valley/test/rules/TestCookingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -17,14 +17,14 @@ def test_can_learn_qos_recipe(self): 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.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -42,21 +42,21 @@ def test_can_learn_qos_recipe(self): 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.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(spring, prevent_sweep=False) + self.multiworld.state.collect(qos, prevent_sweep=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.multiworld.state.collect(self.create_item("Radish Salad Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) def test_get_chefsanity_check_recipe(self): @@ -64,20 +64,20 @@ def test_get_chefsanity_check_recipe(self): 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.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=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.multiworld.state.collect(seeds, prevent_sweep=False) + self.multiworld.state.collect(summer, prevent_sweep=False) + self.multiworld.state.collect(house, prevent_sweep=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.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=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 index 59d41f6a63d6..93c325ae5c5c 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -25,7 +25,7 @@ def test_can_craft_recipe(self): 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.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) def test_can_learn_crafting_recipe(self): @@ -38,16 +38,16 @@ def test_can_learn_crafting_recipe(self): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=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.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -62,16 +62,16 @@ class TestCraftsanityWithFestivalsLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=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.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -92,7 +92,7 @@ def test_can_craft_recipe(self): 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("Pumpkin Seeds"), prevent_sweep=False) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) result = rule(self.multiworld.state) @@ -113,11 +113,11 @@ class TestNoCraftsanityWithFestivalsLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=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.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=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 index 84ceac50ff5a..984a3ebc38b4 100644 --- a/worlds/stardew_valley/test/rules/TestDonations.py +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -18,7 +18,7 @@ def test_cannot_make_any_donation_without_museum_access(self): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -39,7 +39,7 @@ def test_cannot_make_any_donation_without_museum_access(self): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in donation_locations: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -58,7 +58,7 @@ def test_cannot_make_any_donation_without_museum_access(self): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py index 43c5e55c7fca..fb186ca99480 100644 --- a/worlds/stardew_valley/test/rules/TestFriendship.py +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -11,34 +11,34 @@ class TestFriendsanityDatingRules(SVTestBase): 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) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Beach Bridge"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=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) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Weapon"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Barn"), prevent_sweep=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) + self.multiworld.state.collect(self.create_item("Foraging Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Farming Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Mining Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Combat Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=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.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=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): diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py index 378933b7d75d..973d8d3ada7d 100644 --- a/worlds/stardew_valley/test/rules/TestShipping.py +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -76,7 +76,7 @@ def test_all_shipsanity_locations_require_shipping_bin(self): 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) + self.multiworld.state.collect(bin_item, prevent_sweep=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/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py index a1fb152812c8..5f0fe8ef3ffb 100644 --- a/worlds/stardew_valley/test/rules/TestTools.py +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -21,30 +21,30 @@ def test_sturgeon(self): self.assert_rule_false(sturgeon_rule, self.multiworld.state) summer = self.create_item("Summer") - self.multiworld.state.collect(summer, event=False) + self.multiworld.state.collect(summer, prevent_sweep=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.multiworld.state.collect(fishing_rod, prevent_sweep=False) + self.multiworld.state.collect(fishing_rod, prevent_sweep=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.multiworld.state.collect(fishing_level, prevent_sweep=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.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=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.multiworld.state.collect(winter, prevent_sweep=False) self.assert_rule_true(sturgeon_rule, self.multiworld.state) self.remove(fishing_rod) @@ -53,24 +53,24 @@ def test_sturgeon(self): 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.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(fall, prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) tuesday = self.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=False) + self.multiworld.state.collect(tuesday, prevent_sweep=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.multiworld.state.collect(rare_seed, prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) self.remove(fall) @@ -80,11 +80,11 @@ def test_old_master_cannoli(self): green_house = self.create_item("Greenhouse") self.collect(self.create_item(Event.fall_farming)) - self.multiworld.state.collect(green_house, event=False) + self.multiworld.state.collect(green_house, prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) friday = self.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=False) + self.multiworld.state.collect(friday, prevent_sweep=False) self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) self.remove(green_house) @@ -111,7 +111,7 @@ def test_cannot_get_any_tool_without_blacksmith_access(self): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=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]: @@ -125,7 +125,7 @@ def test_cannot_get_fishing_rod_without_willy_access(self): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=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) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py index 77887f8eca0c..972170b93c75 100644 --- a/worlds/stardew_valley/test/rules/TestWeapons.py +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -10,16 +10,16 @@ class TestWeaponsLogic(SVTestBase): } 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.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=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.multiworld.state.collect(self.create_item("Bus Repair"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Skull Key"), prevent_sweep=True) self.GiveItemAndCheckReachableMine("Progressive Sword", 1) self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) @@ -43,7 +43,7 @@ def test_mine(self): 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) + self.multiworld.state.collect(item, prevent_sweep=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) From 8e7ea06f39248b93f02f9640eae9a3d21c805fdb Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 17:17:42 -0500 Subject: [PATCH 143/393] Core: dump all item placements for generation failures. (#3237) * Core: dump all item placements for generation failures * pass the multiworld from remaining fill * change how the args get handled to fix formatting --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 19 +++++++++++++------ Main.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Fill.py b/Fill.py index 5185bbb60ee4..15d5842e2904 100644 --- a/Fill.py +++ b/Fill.py @@ -12,7 +12,12 @@ class FillError(RuntimeError): - pass + def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None: + if "multiworld" in kwargs and isinstance(args[0], str): + placements = (args[0] + f"\nAll Placements:\n" + + f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}") + args = (placements, *args[1:]) + super().__init__(*args) def _log_fill_progress(name: str, placed: int, total_items: int) -> None: @@ -212,7 +217,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) item_pool.extend(unplaced_items) @@ -299,7 +304,7 @@ def remaining_fill(multiworld: MultiWorld, f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) itempool.extend(unplaced_items) @@ -506,7 +511,8 @@ def mark_for_locking(location: Location): if progitempool: raise FillError( f"Not enough locations for progression items. " - f"There are {len(progitempool)} more progression items than there are available locations." + f"There are {len(progitempool)} more progression items than there are available locations.", + multiworld=multiworld, ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) @@ -523,7 +529,8 @@ def mark_for_locking(location: Location): if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " - f"There are {len(excludedlocations)} more excluded locations than filler or trap items." + f"There are {len(excludedlocations)} more excluded locations than filler or trap items.", + multiworld=multiworld, ) restitempool = filleritempool + usefulitempool @@ -589,7 +596,7 @@ def flood_items(multiworld: MultiWorld) -> None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: - raise FillError('No more progress items left to place.') + raise FillError('No more progress items left to place.', multiworld=multiworld) # find item to replace with progress item location_list = multiworld.get_reachable_locations() diff --git a/Main.py b/Main.py index 6dc03aaa55e0..edae5d7b19b1 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,8 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region -from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items +from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ + flood_items from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings @@ -346,7 +347,7 @@ def precollect_hint(location): output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): if not multiworld.can_beat_game(): - raise Exception("Game appears as unbeatable. Aborting.") + raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld) else: logger.warning("Location Accessibility requirements not fulfilled.") From 169da1b1e021bda141f7049cae591bb5c67d37df Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 17:31:26 -0500 Subject: [PATCH 144/393] Tests: fix the all games multiworld test (#3788) --- test/multiworld/test_multiworlds.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 5289cac6c357..8415ac4c8429 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -55,7 +55,7 @@ def test_fills(self) -> None: all_worlds = list(AutoWorldRegister.world_types.values()) self.multiworld = setup_multiworld(all_worlds, ()) for world in self.multiworld.worlds.values(): - world.options.accessibility.value = Accessibility.option_locations + world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) with self.subTest("filling multiworld", seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) @@ -66,8 +66,8 @@ def test_fills(self) -> None: class TestTwoPlayerMulti(MultiworldTestBase): def test_two_player_single_game_fills(self) -> None: """Tests that a multiworld of two players for each registered game world can generate.""" - for world in AutoWorldRegister.world_types.values(): - self.multiworld = setup_multiworld([world, world], ()) + for world_type in AutoWorldRegister.world_types.values(): + self.multiworld = setup_multiworld([world_type, world_type], ()) for world in self.multiworld.worlds.values(): world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) From 0af31c71e0a8e3930cf24aec717fdea644054314 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 13 Aug 2024 20:35:08 -0400 Subject: [PATCH 145/393] TUNIC: Swap from multiworld.get to world.get for applicable things (#3789) * Swap from multiworld.get to world.get for applicable things * Why was this even here in the first place? --- worlds/tunic/__init__.py | 23 ++--- worlds/tunic/er_rules.py | 181 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 2 +- worlds/tunic/rules.py | 180 ++++++++++++++++++------------------ 4 files changed, 190 insertions(+), 196 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 6657b464ed2d..47c66591f912 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -155,8 +155,7 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: if is_mismatched: raise Exception(f"TUNIC: Conflict between seed group {group}'s plando " f"connection {group_cxn.entrance} <-> {group_cxn.exit} and " - f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " - f"connection {cxn.entrance} <-> {cxn.exit}") + f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}") if new_cxn: cls.seed_groups[group]["plando"].value.append(cxn) @@ -187,17 +186,17 @@ def create_items(self) -> None: if self.options.laurels_location: laurels = self.create_item("Hero's Laurels") if self.options.laurels_location == "6_coins": - self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 6 Coins").place_locked_item(laurels) elif self.options.laurels_location == "10_coins": - self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 10 Coins").place_locked_item(laurels) elif self.options.laurels_location == "10_fairies": - self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) + self.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(laurels) items_to_create["Hero's Laurels"] = 0 if self.options.keys_behind_bosses: for rgb_hexagon, location in hexagon_locations.items(): 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.get_location(location).place_locked_item(hex_item) items_to_create[rgb_hexagon] = 0 items_to_create[gold_hexagon] -= 3 @@ -297,15 +296,15 @@ def create_regions(self) -> None: self.multiworld.regions.append(region) for region_name, exits in tunic_regions.items(): - region = self.multiworld.get_region(region_name, self.player) + region = self.get_region(region_name) region.add_exits(exits) for location_name, location_id in self.location_name_to_id.items(): - region = self.multiworld.get_region(location_table[location_name].region, self.player) + region = self.get_region(location_table[location_name].region) location = TunicLocation(self.player, location_name, location_id, region) region.locations.append(location) - victory_region = self.multiworld.get_region("Spirit Arena", self.player) + victory_region = self.get_region("Spirit Arena") victory_location = TunicLocation(self.player, "The Heir", None, victory_region) victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) @@ -339,10 +338,8 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: name, connection = paths[location.parent_region] except KeyError: # logic bug, proceed with warning since it takes a long time to update AP - warning(f"{location.name} is not logically accessible for " - f"{self.multiworld.get_file_safe_player_name(self.player)}. " - "Creating entrance hint Inaccessible. " - "Please report this to the TUNIC rando devs.") + warning(f"{location.name} is not logically accessible for {self.player_name}. " + "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.") hint_text = "Inaccessible" else: while connection != ("Menu", None): diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 81e9d48b4afc..a54ea23bcc0a 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1318,222 +1318,221 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: 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) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), lambda state: state.has("Activate Furnace Fuse", player)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + set_rule(world.get_location("Library Hall - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), + set_rule(world.get_location("Overworld - [Southwest] Flowers Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), + set_rule(world.get_location("Overworld - [East] Weathervane Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), + set_rule(world.get_location("Overworld - [Northeast] Flowers Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), + set_rule(world.get_location("Overworld - [Southwest] Haiku Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), + set_rule(world.get_location("Overworld - [Northwest] Golden Obelisk Page"), lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), + set_rule(world.get_location("Caustic Light Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), + set_rule(world.get_location("Cube Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), + set_rule(world.get_location("Old House - Holy Cross Door Page"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), + set_rule(world.get_location("Maze Cave - Maze Room Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), + set_rule(world.get_location("Patrol Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), + set_rule(world.get_location("Ruined Passage - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), + set_rule(world.get_location("Hourglass Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), + set_rule(world.get_location("Secret Gathering Place - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + set_rule(world.get_location("Coins in the Well - 6 Coins"), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: ( 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), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), 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), + set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) # Ruined Atoll - set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player)) # Beneath the Vault - set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + set_rule(world.get_location("Beneath the Fortress - Bridge"), lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), 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), + set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), 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), + set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), 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), + set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player)) # nmg - kill Librarian with a lure, or gun I guess - set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + set_rule(world.get_location("Librarian - Hexagon Green"), lambda state: (has_sword(state, player) or options.logic_rules) 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), + set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: state.has(fire_wand, player) and has_sword(state, player)) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) # these two swamp checks really want you to kill the big skeleton first - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) # Hero's Grave and Far Shore - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + set_rule(world.get_location("Hero's Grave - Tooth Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + set_rule(world.get_location("Hero's Grave - Ash Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + set_rule(world.get_location("Hero's Grave - Flowers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + set_rule(world.get_location("Hero's Grave - Effigy Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + set_rule(world.get_location("Far Shore - Secret Chest"), lambda state: state.has(laurels, player)) # Events - set_rule(multiworld.get_location("Eastern Bell", player), + set_rule(world.get_location("Eastern Bell"), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Western Bell", player), + set_rule(world.get_location("Western Bell"), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Furnace Fuse", player), + set_rule(world.get_location("Furnace Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), + set_rule(world.get_location("South and West Fortress Exterior Fuses"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), + set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Beneath the Vault Fuse", player), + set_rule(world.get_location("Beneath the Vault Fuse"), lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) - set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + set_rule(world.get_location("Eastern Vault West Fuses"), lambda state: state.has("Activate Beneath the Vault Fuse", player)) - set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + set_rule(world.get_location("Eastern Vault East Fuse"), 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), + set_rule(world.get_location("Quarry Connector Fuse"), lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) - set_rule(multiworld.get_location("Quarry Fuse", player), + set_rule(world.get_location("Quarry Fuse"), lambda state: state.has("Activate Quarry Connector Fuse", player)) - set_rule(multiworld.get_location("Ziggurat Fuse", player), + set_rule(world.get_location("Ziggurat Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("West Garden Fuse", player), + set_rule(world.get_location("West Garden Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Library Fuse", player), + set_rule(world.get_location("Library Fuse"), lambda state: has_ability(prayer, state, world)) # Shop - set_rule(multiworld.get_location("Shop - Potion 1", player), + set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Potion 2", player), + set_rule(world.get_location("Shop - Potion 2"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 1", player), + set_rule(world.get_location("Shop - Coin 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 2", player), + set_rule(world.get_location("Shop - Coin 2"), lambda state: has_sword(state, player)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index a4295cf9f2a4..e7c8fd58d0c6 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -130,7 +130,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} dead_ends: List[Portal] = [] two_plus: List[Portal] = [] - player_name = world.multiworld.get_player_name(world.player) + player_name = world.player_name portal_map = portal_mapping.copy() logic_rules = world.options.logic_rules.value fixed_shop = world.options.fixed_shop diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 73eb8118901b..2ff588da904d 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -87,41 +87,40 @@ def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: 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 = \ + world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \ lambda state: has_ability(holy_cross, state, world) - multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ + world.get_entrance("Overworld -> Beneath the Well").access_rule = \ lambda state: has_stick(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ + world.get_entrance("Overworld -> Dark Tomb").access_rule = \ lambda state: has_lantern(state, world) - multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ + world.get_entrance("Overworld -> West Garden").access_rule = \ lambda state: state.has(laurels, player) \ or can_ladder_storage(state, world) - multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ + world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ lambda state: state.has(laurels, player) \ 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 = \ + world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ lambda state: has_lantern(state, world) and has_ability(prayer, state, world) - multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ + world.get_entrance("Ruined Atoll -> Library").access_rule = \ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) - multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ + world.get_entrance("Overworld -> Quarry").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, world)) - multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ + world.get_entrance("Quarry Back -> Quarry").access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ + world.get_entrance("Quarry -> Lower Quarry").access_rule = \ lambda state: has_mask(state, world) - multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ + world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) - multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ + world.get_entrance("Swamp -> Cathedral").access_rule = \ 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 = \ + world.get_entrance("Overworld -> Spirit Arena").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 state.has_group_unique("Hero Relics", player, 6)) @@ -130,210 +129,209 @@ def set_region_rules(world: "TunicWorld") -> None: def set_location_rules(world: "TunicWorld") -> None: - multiworld = world.multiworld player = world.player options = world.options - forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("Far Shore - Page Pickup", player), + set_rule(world.get_location("Far Shore - Page Pickup"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), + set_rule(world.get_location("Fortress Courtyard - Chest Near Cave"), 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), + set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), 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), + set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + set_rule(world.get_location("Library Hall - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + set_rule(world.get_location("Far Shore - Secret Chest"), 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), + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Normal Chest", player), + set_rule(world.get_location("Old House - Normal Chest"), lambda state: 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 - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), 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), + set_rule(world.get_location("Old House - Shield Pickup"), lambda state: 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("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + set_rule(world.get_location("Overworld - [West] Chest After Bell"), lambda state: state.has(laurels, player) 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), + set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + set_rule(world.get_location("Special Shop - Secret Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), 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), + set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: 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("West Furnace - Lantern Pickup", player), + set_rule(world.get_location("West Furnace - Lantern Pickup"), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + set_rule(world.get_location("Coins in the Well - 6 Coins"), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: 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), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), 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), + set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), 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), + set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), lambda state: state.has(laurels, player) 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), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + set_rule(world.get_location("Librarian - Hexagon Green"), lambda state: has_sword(state, player) or options.logic_rules) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), + set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), 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), + set_rule(world.get_location("Fortress Arena - Hexagon Red"), 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), + set_rule(world.get_location("Beneath the Fortress - Bridge"), 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), + set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"), lambda state: has_stick(state, player) and has_lantern(state, world)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), 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), + set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + set_rule(world.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) # Hero's Grave - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + set_rule(world.get_location("Hero's Grave - Tooth Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + set_rule(world.get_location("Hero's Grave - Ash Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + set_rule(world.get_location("Hero's Grave - Flowers Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + set_rule(world.get_location("Hero's Grave - Effigy Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) # Shop - set_rule(multiworld.get_location("Shop - Potion 1", player), + set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Potion 2", player), + set_rule(world.get_location("Shop - Potion 2"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 1", player), + set_rule(world.get_location("Shop - Coin 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 2", player), + set_rule(world.get_location("Shop - Coin 2"), lambda state: has_sword(state, player)) From 9fbaa6050f40aded0546ea143743e1b3db559aa9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 14 Aug 2024 00:21:42 -0400 Subject: [PATCH 146/393] I have no idea (#3791) --- WebHostLib/templates/weightedOptions/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index a1d319697154..68d3968a178a 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -138,7 +138,7 @@ id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" - checked="{{ "checked" if key in option.default else "" }}" + {{ "checked" if key in option.default }} />
From a40744e6db904d6445f7c39c4117fb2e2096edcb Mon Sep 17 00:00:00 2001 From: Spineraks Date: Fri, 6 Sep 2024 22:50:57 +0200 Subject: [PATCH 230/393] Yacht Dice: logic fix and several other fixes (#3878) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/Rules.py | 2 +- worlds/yachtdice/YachtWeights.py | 3040 +++++++++--------------------- worlds/yachtdice/__init__.py | 40 +- 3 files changed, 938 insertions(+), 2144 deletions(-) diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py index 1db5cebccdef..d99f5b147493 100644 --- a/worlds/yachtdice/Rules.py +++ b/worlds/yachtdice/Rules.py @@ -29,7 +29,7 @@ def mean_score(self, num_dice, num_rolls): mean_score = 0 for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items(): mean_score += key * value / 100000 - return mean_score * self.quantity + return mean_score class ListState: diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py index ee387fdf212d..5f647f3420ba 100644 --- a/worlds/yachtdice/YachtWeights.py +++ b/worlds/yachtdice/YachtWeights.py @@ -17,77 +17,77 @@ ("Category Ones", 0, 7): {0: 100000}, ("Category Ones", 0, 8): {0: 100000}, ("Category Ones", 1, 0): {0: 100000}, - ("Category Ones", 1, 1): {0: 83416, 1: 16584}, - ("Category Ones", 1, 2): {0: 69346, 1: 30654}, - ("Category Ones", 1, 3): {0: 57756, 1: 42244}, - ("Category Ones", 1, 4): {0: 48709, 1: 51291}, - ("Category Ones", 1, 5): {0: 40214, 1: 59786}, + ("Category Ones", 1, 1): {0: 100000}, + ("Category Ones", 1, 2): {0: 100000}, + ("Category Ones", 1, 3): {0: 100000}, + ("Category Ones", 1, 4): {0: 100000}, + ("Category Ones", 1, 5): {0: 100000}, ("Category Ones", 1, 6): {0: 33491, 1: 66509}, ("Category Ones", 1, 7): {0: 27838, 1: 72162}, ("Category Ones", 1, 8): {0: 23094, 1: 76906}, ("Category Ones", 2, 0): {0: 100000}, - ("Category Ones", 2, 1): {0: 69715, 1: 30285}, - ("Category Ones", 2, 2): {0: 48066, 1: 51934}, - ("Category Ones", 2, 3): {0: 33544, 1: 48585, 2: 17871}, - ("Category Ones", 2, 4): {0: 23342, 1: 50092, 2: 26566}, - ("Category Ones", 2, 5): {0: 16036, 1: 48250, 2: 35714}, - ("Category Ones", 2, 6): {0: 11355, 1: 44545, 2: 44100}, - ("Category Ones", 2, 7): {0: 7812, 1: 40248, 2: 51940}, - ("Category Ones", 2, 8): {0: 5395, 1: 35484, 2: 59121}, + ("Category Ones", 2, 1): {0: 100000}, + ("Category Ones", 2, 2): {0: 100000}, + ("Category Ones", 2, 3): {0: 33544, 1: 66456}, + ("Category Ones", 2, 4): {0: 23342, 1: 76658}, + ("Category Ones", 2, 5): {0: 16036, 2: 83964}, + ("Category Ones", 2, 6): {0: 11355, 2: 88645}, + ("Category Ones", 2, 7): {0: 7812, 2: 92188}, + ("Category Ones", 2, 8): {0: 5395, 2: 94605}, ("Category Ones", 3, 0): {0: 100000}, - ("Category Ones", 3, 1): {0: 57462, 1: 42538}, - ("Category Ones", 3, 2): {0: 33327, 1: 44253, 2: 22420}, - ("Category Ones", 3, 3): {0: 19432, 1: 42237, 2: 38331}, - ("Category Ones", 3, 4): {0: 11191, 1: 36208, 2: 38606, 3: 13995}, - ("Category Ones", 3, 5): {0: 6536, 1: 28891, 2: 43130, 3: 21443}, - ("Category Ones", 3, 6): {0: 3697, 1: 22501, 2: 44196, 3: 29606}, - ("Category Ones", 3, 7): {0: 2134, 2: 60499, 3: 37367}, - ("Category Ones", 3, 8): {0: 1280, 2: 53518, 3: 45202}, + ("Category Ones", 3, 1): {0: 100000}, + ("Category Ones", 3, 2): {0: 33327, 1: 66673}, + ("Category Ones", 3, 3): {0: 19432, 2: 80568}, + ("Category Ones", 3, 4): {0: 11191, 2: 88809}, + ("Category Ones", 3, 5): {0: 35427, 2: 64573}, + ("Category Ones", 3, 6): {0: 26198, 2: 73802}, + ("Category Ones", 3, 7): {0: 18851, 3: 81149}, + ("Category Ones", 3, 8): {0: 13847, 3: 86153}, ("Category Ones", 4, 0): {0: 100000}, - ("Category Ones", 4, 1): {0: 48178, 1: 38635, 2: 13187}, - ("Category Ones", 4, 2): {0: 23349, 1: 40775, 2: 35876}, - ("Category Ones", 4, 3): {0: 11366, 1: 32547, 2: 35556, 3: 20531}, - ("Category Ones", 4, 4): {0: 5331, 1: 23241, 2: 37271, 3: 34157}, - ("Category Ones", 4, 5): {0: 2640, 2: 49872, 3: 47488}, - ("Category Ones", 4, 6): {0: 1253, 2: 39816, 3: 39298, 4: 19633}, - ("Category Ones", 4, 7): {0: 6915, 2: 24313, 3: 41680, 4: 27092}, - ("Category Ones", 4, 8): {0: 4228, 3: 61312, 4: 34460}, + ("Category Ones", 4, 1): {0: 100000}, + ("Category Ones", 4, 2): {0: 23349, 2: 76651}, + ("Category Ones", 4, 3): {0: 11366, 2: 88634}, + ("Category Ones", 4, 4): {0: 28572, 3: 71428}, + ("Category Ones", 4, 5): {0: 17976, 3: 82024}, + ("Category Ones", 4, 6): {0: 1253, 3: 98747}, + ("Category Ones", 4, 7): {0: 31228, 3: 68772}, + ("Category Ones", 4, 8): {0: 23273, 4: 76727}, ("Category Ones", 5, 0): {0: 100000}, - ("Category Ones", 5, 1): {0: 40042, 1: 40202, 2: 19756}, - ("Category Ones", 5, 2): {0: 16212, 1: 35432, 2: 31231, 3: 17125}, - ("Category Ones", 5, 3): {0: 6556, 1: 23548, 2: 34509, 3: 35387}, - ("Category Ones", 5, 4): {0: 2552, 2: 44333, 3: 32048, 4: 21067}, - ("Category Ones", 5, 5): {0: 8783, 2: 23245, 3: 34614, 4: 33358}, - ("Category Ones", 5, 6): {0: 4513, 3: 49603, 4: 32816, 5: 13068}, - ("Category Ones", 5, 7): {0: 2295, 3: 40470, 4: 37869, 5: 19366}, - ("Category Ones", 5, 8): {0: 73, 3: 33115, 4: 40166, 5: 26646}, + ("Category Ones", 5, 1): {0: 100000}, + ("Category Ones", 5, 2): {0: 16212, 2: 83788}, + ("Category Ones", 5, 3): {0: 30104, 3: 69896}, + ("Category Ones", 5, 4): {0: 2552, 3: 97448}, + ("Category Ones", 5, 5): {0: 32028, 4: 67972}, + ("Category Ones", 5, 6): {0: 21215, 4: 78785}, + ("Category Ones", 5, 7): {0: 2295, 4: 97705}, + ("Category Ones", 5, 8): {0: 1167, 4: 98833}, ("Category Ones", 6, 0): {0: 100000}, - ("Category Ones", 6, 1): {0: 33501, 1: 40042, 2: 26457}, - ("Category Ones", 6, 2): {0: 11326, 1: 29379, 2: 32368, 3: 26927}, - ("Category Ones", 6, 3): {0: 3764, 2: 46660, 3: 28928, 4: 20648}, - ("Category Ones", 6, 4): {0: 1231, 2: 29883, 3: 31038, 4: 37848}, - ("Category Ones", 6, 5): {0: 4208, 3: 41897, 4: 30878, 5: 23017}, - ("Category Ones", 6, 6): {0: 1850, 3: 30396, 4: 33022, 5: 34732}, - ("Category Ones", 6, 7): {0: 5503, 4: 48099, 5: 32432, 6: 13966}, - ("Category Ones", 6, 8): {0: 2896, 4: 39616, 5: 37005, 6: 20483}, + ("Category Ones", 6, 1): {0: 33501, 1: 66499}, + ("Category Ones", 6, 2): {0: 40705, 2: 59295}, + ("Category Ones", 6, 3): {0: 3764, 3: 96236}, + ("Category Ones", 6, 4): {0: 9324, 4: 90676}, + ("Category Ones", 6, 5): {0: 4208, 4: 95792}, + ("Category Ones", 6, 6): {0: 158, 5: 99842}, + ("Category Ones", 6, 7): {0: 5503, 5: 94497}, + ("Category Ones", 6, 8): {0: 2896, 5: 97104}, ("Category Ones", 7, 0): {0: 100000}, - ("Category Ones", 7, 1): {0: 27838, 1: 39224, 2: 32938}, - ("Category Ones", 7, 2): {0: 7796, 1: 23850, 2: 31678, 3: 23224, 4: 13452}, - ("Category Ones", 7, 3): {0: 2247, 2: 35459, 3: 29131, 4: 33163}, - ("Category Ones", 7, 4): {0: 5252, 3: 41207, 4: 28065, 5: 25476}, - ("Category Ones", 7, 5): {0: 174, 3: 29347, 4: 28867, 5: 26190, 6: 15422}, - ("Category Ones", 7, 6): {0: 4625, 4: 38568, 5: 30596, 6: 26211}, - ("Category Ones", 7, 7): {0: 230, 4: 30109, 5: 32077, 6: 37584}, - ("Category Ones", 7, 8): {0: 5519, 5: 45718, 6: 33357, 7: 15406}, + ("Category Ones", 7, 1): {0: 27838, 2: 72162}, + ("Category Ones", 7, 2): {0: 7796, 3: 92204}, + ("Category Ones", 7, 3): {0: 13389, 4: 86611}, + ("Category Ones", 7, 4): {0: 5252, 4: 94748}, + ("Category Ones", 7, 5): {0: 9854, 5: 90146}, + ("Category Ones", 7, 6): {0: 4625, 5: 95375}, + ("Category Ones", 7, 7): {0: 30339, 6: 69661}, + ("Category Ones", 7, 8): {0: 5519, 6: 94481}, ("Category Ones", 8, 0): {0: 100000}, - ("Category Ones", 8, 1): {0: 23156, 1: 37295, 2: 26136, 3: 13413}, - ("Category Ones", 8, 2): {0: 5472, 2: 48372, 3: 25847, 4: 20309}, - ("Category Ones", 8, 3): {0: 8661, 3: 45896, 4: 24664, 5: 20779}, - ("Category Ones", 8, 4): {0: 2807, 3: 29707, 4: 27157, 5: 23430, 6: 16899}, - ("Category Ones", 8, 5): {0: 5173, 4: 36033, 5: 27792, 6: 31002}, - ("Category Ones", 8, 6): {0: 255, 4: 25642, 5: 27508, 6: 27112, 7: 19483}, - ("Category Ones", 8, 7): {0: 4236, 5: 35323, 6: 30438, 7: 30003}, - ("Category Ones", 8, 8): {0: 310, 5: 27692, 6: 30830, 7: 41168}, + ("Category Ones", 8, 1): {0: 23156, 2: 76844}, + ("Category Ones", 8, 2): {0: 5472, 3: 94528}, + ("Category Ones", 8, 3): {0: 8661, 4: 91339}, + ("Category Ones", 8, 4): {0: 12125, 5: 87875}, + ("Category Ones", 8, 5): {0: 5173, 5: 94827}, + ("Category Ones", 8, 6): {0: 8872, 6: 91128}, + ("Category Ones", 8, 7): {0: 4236, 6: 95764}, + ("Category Ones", 8, 8): {0: 9107, 7: 90893}, ("Category Twos", 0, 0): {0: 100000}, ("Category Twos", 0, 1): {0: 100000}, ("Category Twos", 0, 2): {0: 100000}, @@ -98,8 +98,8 @@ ("Category Twos", 0, 7): {0: 100000}, ("Category Twos", 0, 8): {0: 100000}, ("Category Twos", 1, 0): {0: 100000}, - ("Category Twos", 1, 1): {0: 83475, 2: 16525}, - ("Category Twos", 1, 2): {0: 69690, 2: 30310}, + ("Category Twos", 1, 1): {0: 100000}, + ("Category Twos", 1, 2): {0: 100000}, ("Category Twos", 1, 3): {0: 57818, 2: 42182}, ("Category Twos", 1, 4): {0: 48418, 2: 51582}, ("Category Twos", 1, 5): {0: 40301, 2: 59699}, @@ -107,68 +107,68 @@ ("Category Twos", 1, 7): {0: 28182, 2: 71818}, ("Category Twos", 1, 8): {0: 23406, 2: 76594}, ("Category Twos", 2, 0): {0: 100000}, - ("Category Twos", 2, 1): {0: 69724, 2: 30276}, - ("Category Twos", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Twos", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Twos", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Twos", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Twos", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Twos", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Twos", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Twos", 2, 1): {0: 100000}, + ("Category Twos", 2, 2): {0: 48238, 2: 51762}, + ("Category Twos", 2, 3): {0: 33290, 4: 66710}, + ("Category Twos", 2, 4): {0: 23136, 4: 76864}, + ("Category Twos", 2, 5): {0: 16146, 4: 83854}, + ("Category Twos", 2, 6): {0: 11083, 4: 88917}, + ("Category Twos", 2, 7): {0: 7662, 4: 92338}, + ("Category Twos", 2, 8): {0: 5354, 4: 94646}, ("Category Twos", 3, 0): {0: 100000}, - ("Category Twos", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Twos", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Twos", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Twos", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Twos", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Twos", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Twos", 3, 1): {0: 58021, 2: 41979}, + ("Category Twos", 3, 2): {0: 33548, 4: 66452}, + ("Category Twos", 3, 3): {0: 19375, 4: 80625}, + ("Category Twos", 3, 4): {0: 10998, 4: 89002}, + ("Category Twos", 3, 5): {0: 6519, 6: 93481}, + ("Category Twos", 3, 6): {0: 3619, 6: 96381}, + ("Category Twos", 3, 7): {0: 2195, 6: 97805}, + ("Category Twos", 3, 8): {0: 13675, 6: 86325}, ("Category Twos", 4, 0): {0: 100000}, - ("Category Twos", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Twos", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Twos", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Twos", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Twos", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Twos", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Twos", 4, 1): {0: 48235, 2: 51765}, + ("Category Twos", 4, 2): {0: 23289, 4: 76711}, + ("Category Twos", 4, 3): {0: 11177, 6: 88823}, + ("Category Twos", 4, 4): {0: 5499, 6: 94501}, + ("Category Twos", 4, 5): {0: 18356, 6: 81644}, + ("Category Twos", 4, 6): {0: 11169, 8: 88831}, + ("Category Twos", 4, 7): {0: 6945, 8: 93055}, + ("Category Twos", 4, 8): {0: 4091, 8: 95909}, ("Category Twos", 5, 0): {0: 100000}, - ("Category Twos", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Twos", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Twos", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Twos", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Twos", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Twos", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Twos", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Twos", 5, 1): {0: 40028, 4: 59972}, + ("Category Twos", 5, 2): {0: 16009, 6: 83991}, + ("Category Twos", 5, 3): {0: 6489, 6: 93511}, + ("Category Twos", 5, 4): {0: 16690, 8: 83310}, + ("Category Twos", 5, 5): {0: 9016, 8: 90984}, + ("Category Twos", 5, 6): {0: 4602, 8: 95398}, + ("Category Twos", 5, 7): {0: 13627, 10: 86373}, + ("Category Twos", 5, 8): {0: 8742, 10: 91258}, ("Category Twos", 6, 0): {0: 100000}, - ("Category Twos", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Twos", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Twos", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Twos", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Twos", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Twos", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Twos", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Twos", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Twos", 6, 1): {0: 33502, 4: 66498}, + ("Category Twos", 6, 2): {0: 11210, 6: 88790}, + ("Category Twos", 6, 3): {0: 3673, 6: 96327}, + ("Category Twos", 6, 4): {0: 9291, 8: 90709}, + ("Category Twos", 6, 5): {0: 441, 8: 99559}, + ("Category Twos", 6, 6): {0: 10255, 10: 89745}, + ("Category Twos", 6, 7): {0: 5646, 10: 94354}, + ("Category Twos", 6, 8): {0: 14287, 12: 85713}, ("Category Twos", 7, 0): {0: 100000}, - ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Twos", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Twos", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Twos", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Twos", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Twos", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Twos", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Twos", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Twos", 7, 1): {0: 27683, 4: 72317}, + ("Category Twos", 7, 2): {0: 7824, 6: 92176}, + ("Category Twos", 7, 3): {0: 13167, 8: 86833}, + ("Category Twos", 7, 4): {0: 564, 10: 99436}, + ("Category Twos", 7, 5): {0: 9824, 10: 90176}, + ("Category Twos", 7, 6): {0: 702, 12: 99298}, + ("Category Twos", 7, 7): {0: 10186, 12: 89814}, + ("Category Twos", 7, 8): {0: 942, 12: 99058}, ("Category Twos", 8, 0): {0: 100000}, - ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Twos", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Twos", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Twos", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Twos", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Twos", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Twos", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Twos", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Twos", 8, 1): {0: 23378, 4: 76622}, + ("Category Twos", 8, 2): {0: 5420, 8: 94580}, + ("Category Twos", 8, 3): {0: 8560, 10: 91440}, + ("Category Twos", 8, 4): {0: 12199, 12: 87801}, + ("Category Twos", 8, 5): {0: 879, 12: 99121}, + ("Category Twos", 8, 6): {0: 9033, 14: 90967}, + ("Category Twos", 8, 7): {0: 15767, 14: 84233}, + ("Category Twos", 8, 8): {2: 9033, 14: 90967}, ("Category Threes", 0, 0): {0: 100000}, ("Category Threes", 0, 1): {0: 100000}, ("Category Threes", 0, 2): {0: 100000}, @@ -179,7 +179,7 @@ ("Category Threes", 0, 7): {0: 100000}, ("Category Threes", 0, 8): {0: 100000}, ("Category Threes", 1, 0): {0: 100000}, - ("Category Threes", 1, 1): {0: 83343, 3: 16657}, + ("Category Threes", 1, 1): {0: 100000}, ("Category Threes", 1, 2): {0: 69569, 3: 30431}, ("Category Threes", 1, 3): {0: 57872, 3: 42128}, ("Category Threes", 1, 4): {0: 48081, 3: 51919}, @@ -189,67 +189,67 @@ ("Category Threes", 1, 8): {0: 23240, 3: 76760}, ("Category Threes", 2, 0): {0: 100000}, ("Category Threes", 2, 1): {0: 69419, 3: 30581}, - ("Category Threes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Threes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Threes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Threes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Threes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Threes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Threes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Threes", 2, 2): {0: 48202, 3: 51798}, + ("Category Threes", 2, 3): {0: 33376, 6: 66624}, + ("Category Threes", 2, 4): {0: 23276, 6: 76724}, + ("Category Threes", 2, 5): {0: 16092, 6: 83908}, + ("Category Threes", 2, 6): {0: 11232, 6: 88768}, + ("Category Threes", 2, 7): {0: 7589, 6: 92411}, + ("Category Threes", 2, 8): {0: 5447, 6: 94553}, ("Category Threes", 3, 0): {0: 100000}, - ("Category Threes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Threes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Threes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Threes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Threes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Threes", 3, 1): {0: 57964, 3: 42036}, + ("Category Threes", 3, 2): {0: 33637, 6: 66363}, + ("Category Threes", 3, 3): {0: 19520, 6: 80480}, + ("Category Threes", 3, 4): {0: 11265, 6: 88735}, + ("Category Threes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Threes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Threes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Threes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Threes", 4, 0): {0: 100000}, - ("Category Threes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Threes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Threes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Threes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Threes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Threes", 4, 1): {0: 48121, 6: 51879}, + ("Category Threes", 4, 2): {0: 23296, 6: 76704}, + ("Category Threes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Threes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Threes", 4, 6): {0: 11267, 9: 88733}, + ("Category Threes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Threes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Threes", 5, 0): {0: 100000}, - ("Category Threes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Threes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Threes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Threes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Threes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Threes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Threes", 5, 1): {0: 40183, 6: 59817}, + ("Category Threes", 5, 2): {0: 16197, 6: 83803}, + ("Category Threes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Threes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Threes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Threes", 5, 6): {0: 4652, 12: 95348}, + ("Category Threes", 5, 7): {0: 2365, 12: 97635}, + ("Category Threes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Threes", 6, 0): {0: 100000}, - ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Threes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Threes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Threes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Threes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Threes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Threes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Threes", 6, 1): {0: 33473, 6: 66527}, + ("Category Threes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Threes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Threes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Threes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Threes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Threes", 6, 7): {0: 5519, 15: 94481}, + ("Category Threes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Threes", 7, 0): {0: 100000}, - ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Threes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Threes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Threes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Threes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Threes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Threes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Threes", 7, 1): {0: 27933, 6: 72067}, + ("Category Threes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Threes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Threes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Threes", 7, 5): {0: 9894, 15: 90106}, + ("Category Threes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Threes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Threes", 7, 8): {0: 5710, 18: 94290}, ("Category Threes", 8, 0): {0: 100000}, - ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Threes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Threes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Threes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Threes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Threes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Threes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Threes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, + ("Category Threes", 8, 1): {0: 23337, 6: 76663}, + ("Category Threes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Threes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Threes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Threes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Threes", 8, 6): {0: 8804, 18: 91196}, + ("Category Threes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Threes", 8, 8): {0: 9252, 21: 90748}, ("Category Fours", 0, 0): {0: 100000}, ("Category Fours", 0, 1): {0: 100000}, ("Category Fours", 0, 2): {0: 100000}, @@ -270,67 +270,67 @@ ("Category Fours", 1, 8): {0: 23431, 4: 76569}, ("Category Fours", 2, 0): {0: 100000}, ("Category Fours", 2, 1): {0: 69379, 4: 30621}, - ("Category Fours", 2, 2): {0: 48538, 4: 42240, 8: 9222}, + ("Category Fours", 2, 2): {0: 48538, 4: 51462}, ("Category Fours", 2, 3): {0: 33756, 4: 48555, 8: 17689}, ("Category Fours", 2, 4): {0: 23070, 4: 49916, 8: 27014}, ("Category Fours", 2, 5): {0: 16222, 4: 48009, 8: 35769}, ("Category Fours", 2, 6): {0: 11125, 4: 44400, 8: 44475}, ("Category Fours", 2, 7): {0: 7919, 4: 40216, 8: 51865}, - ("Category Fours", 2, 8): {0: 5348, 4: 35757, 8: 58895}, + ("Category Fours", 2, 8): {0: 5348, 8: 94652}, ("Category Fours", 3, 0): {0: 100000}, - ("Category Fours", 3, 1): {0: 57914, 4: 34622, 8: 7464}, + ("Category Fours", 3, 1): {0: 57914, 4: 42086}, ("Category Fours", 3, 2): {0: 33621, 4: 44110, 8: 22269}, - ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 30898, 12: 7524}, - ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 39024, 12: 13840}, - ("Category Fours", 3, 5): {0: 6367, 4: 29116, 8: 43192, 12: 21325}, - ("Category Fours", 3, 6): {0: 3643, 4: 22457, 8: 44477, 12: 29423}, - ("Category Fours", 3, 7): {0: 2178, 4: 16802, 8: 43275, 12: 37745}, - ("Category Fours", 3, 8): {0: 1255, 4: 12301, 8: 41132, 12: 45312}, + ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 38422}, + ("Category Fours", 3, 4): {0: 11125, 8: 88875}, + ("Category Fours", 3, 5): {0: 6367, 8: 72308, 12: 21325}, + ("Category Fours", 3, 6): {0: 3643, 8: 66934, 12: 29423}, + ("Category Fours", 3, 7): {0: 2178, 8: 60077, 12: 37745}, + ("Category Fours", 3, 8): {0: 1255, 8: 53433, 12: 45312}, ("Category Fours", 4, 0): {0: 100000}, - ("Category Fours", 4, 1): {0: 48465, 4: 38398, 8: 13137}, - ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 27073, 12: 8720}, - ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, - ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 26861, 16: 7185}, - ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 34222, 16: 12796}, - ("Category Fours", 4, 6): {0: 1314, 4: 10001, 8: 29850, 12: 39425, 16: 19410}, - ("Category Fours", 4, 7): {0: 592, 4: 6231, 8: 24250, 12: 41917, 16: 27010}, - ("Category Fours", 4, 8): {0: 302, 8: 23055, 12: 41866, 16: 34777}, + ("Category Fours", 4, 1): {0: 48465, 4: 51535}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 12: 35793}, + ("Category Fours", 4, 3): {0: 11200, 8: 68528, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 8: 60507, 12: 34046}, + ("Category Fours", 4, 5): {0: 2533, 8: 50449, 16: 47018}, + ("Category Fours", 4, 6): {0: 1314, 8: 39851, 12: 39425, 16: 19410}, + ("Category Fours", 4, 7): {0: 6823, 12: 66167, 16: 27010}, + ("Category Fours", 4, 8): {0: 4189, 12: 61034, 16: 34777}, ("Category Fours", 5, 0): {0: 100000}, - ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 16028, 12: 3630}, - ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 13998, 16: 3319}, - ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 24783, 16: 10458}, - ("Category Fours", 5, 4): {0: 2635, 4: 13889, 8: 30079, 12: 32428, 16: 17263, 20: 3706}, - ("Category Fours", 5, 5): {0: 1160, 4: 7756, 8: 23332, 12: 34254, 16: 25803, 20: 7695}, - ("Category Fours", 5, 6): {0: 434, 8: 20773, 12: 32910, 16: 32752, 20: 13131}, - ("Category Fours", 5, 7): {0: 169, 8: 13536, 12: 29123, 16: 37701, 20: 19471}, - ("Category Fours", 5, 8): {0: 1267, 8: 7340, 12: 24807, 16: 40144, 20: 26442}, + ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 19658}, + ("Category Fours", 5, 2): {0: 15946, 8: 66737, 12: 17317}, + ("Category Fours", 5, 3): {0: 6479, 8: 58280, 16: 35241}, + ("Category Fours", 5, 4): {0: 2635, 8: 43968, 16: 53397}, + ("Category Fours", 5, 5): {0: 8916, 12: 57586, 16: 33498}, + ("Category Fours", 5, 6): {0: 4682, 12: 49435, 20: 45883}, + ("Category Fours", 5, 7): {0: 2291, 12: 40537, 16: 37701, 20: 19471}, + ("Category Fours", 5, 8): {0: 75, 16: 73483, 20: 26442}, ("Category Fours", 6, 0): {0: 100000}, - ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 20225, 12: 6287}, - ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 19179, 16: 7441}, - ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 15808, 20: 5155}, - ("Category Fours", 6, 4): {0: 1284, 4: 7889, 8: 21748, 12: 31107, 16: 25281, 20: 12691}, - ("Category Fours", 6, 5): {0: 462, 8: 17601, 12: 27817, 16: 31233, 20: 18386, 24: 4501}, - ("Category Fours", 6, 6): {0: 1783, 8: 8344, 12: 22156, 16: 32690, 20: 26192, 24: 8835}, - ("Category Fours", 6, 7): {0: 767, 12: 20974, 16: 31490, 20: 32639, 24: 14130}, - ("Category Fours", 6, 8): {0: 357, 12: 13912, 16: 27841, 20: 37380, 24: 20510}, + ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 26512}, + ("Category Fours", 6, 2): {0: 11175, 8: 62205, 12: 26620}, + ("Category Fours", 6, 3): {0: 3698, 8: 46268, 16: 50034}, + ("Category Fours", 6, 4): {0: 9173, 12: 52855, 20: 37972}, + ("Category Fours", 6, 5): {0: 4254, 12: 41626, 20: 54120}, + ("Category Fours", 6, 6): {0: 1783, 16: 63190, 24: 35027}, + ("Category Fours", 6, 7): {0: 5456, 16: 47775, 24: 46769}, + ("Category Fours", 6, 8): {0: 2881, 16: 39229, 24: 57890}, ("Category Fours", 7, 0): {0: 100000}, - ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 23327, 12: 9563}, - ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 23169, 16: 13222}, - ("Category Fours", 7, 3): {0: 2194, 4: 11153, 8: 24107, 12: 29411, 16: 21390, 20: 11745}, - ("Category Fours", 7, 4): {0: 560, 8: 19291, 12: 26330, 16: 28118, 20: 18174, 24: 7527}, - ("Category Fours", 7, 5): {0: 1858, 8: 7862, 12: 19425, 16: 29003, 20: 26113, 24: 15739}, - ("Category Fours", 7, 6): {0: 679, 12: 16759, 16: 25831, 20: 30724, 24: 20147, 28: 5860}, - ("Category Fours", 7, 7): {0: 13, 12: 10063, 16: 20524, 20: 31843, 24: 27368, 28: 10189}, - ("Category Fours", 7, 8): {4: 864, 16: 19910, 20: 30153, 24: 33428, 28: 15645}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 12: 32890}, + ("Category Fours", 7, 2): {0: 7950, 8: 55659, 16: 36391}, + ("Category Fours", 7, 3): {0: 2194, 12: 64671, 20: 33135}, + ("Category Fours", 7, 4): {0: 5063, 12: 41118, 20: 53819}, + ("Category Fours", 7, 5): {0: 171, 16: 57977, 24: 41852}, + ("Category Fours", 7, 6): {0: 4575, 16: 38694, 24: 56731}, + ("Category Fours", 7, 7): {0: 252, 20: 62191, 28: 37557}, + ("Category Fours", 7, 8): {4: 5576, 20: 45351, 28: 49073}, ("Category Fours", 8, 0): {0: 100000}, - ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 25964, 12: 13600}, - ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 14387, 20: 6107}, - ("Category Fours", 8, 3): {0: 1277, 4: 7349, 8: 18330, 12: 27186, 16: 25138, 20: 14371, 24: 6349}, - ("Category Fours", 8, 4): {0: 289, 8: 11929, 12: 20282, 16: 26960, 20: 23292, 24: 12927, 28: 4321}, - ("Category Fours", 8, 5): {0: 835, 12: 16706, 16: 23588, 20: 27754, 24: 20767, 28: 10350}, - ("Category Fours", 8, 6): {0: 21, 12: 8911, 16: 17296, 20: 27398, 24: 27074, 28: 15457, 32: 3843}, - ("Category Fours", 8, 7): {0: 745, 16: 15069, 20: 23737, 24: 30628, 28: 22590, 32: 7231}, - ("Category Fours", 8, 8): {0: 1949, 16: 7021, 20: 18630, 24: 31109, 28: 29548, 32: 11743}, + ("Category Fours", 8, 1): {0: 23275, 8: 76725}, + ("Category Fours", 8, 2): {0: 5421, 8: 48273, 16: 46306}, + ("Category Fours", 8, 3): {0: 8626, 12: 45516, 20: 45858}, + ("Category Fours", 8, 4): {0: 2852, 16: 56608, 24: 40540}, + ("Category Fours", 8, 5): {0: 5049, 20: 63834, 28: 31117}, + ("Category Fours", 8, 6): {0: 269, 20: 53357, 28: 46374}, + ("Category Fours", 8, 7): {0: 4394, 24: 65785, 28: 29821}, + ("Category Fours", 8, 8): {0: 266, 24: 58443, 32: 41291}, ("Category Fives", 0, 0): {0: 100000}, ("Category Fives", 0, 1): {0: 100000}, ("Category Fives", 0, 2): {0: 100000}, @@ -350,8 +350,8 @@ ("Category Fives", 1, 7): {0: 27730, 5: 72270}, ("Category Fives", 1, 8): {0: 23210, 5: 76790}, ("Category Fives", 2, 0): {0: 100000}, - ("Category Fives", 2, 1): {0: 69299, 5: 27864, 10: 2837}, - ("Category Fives", 2, 2): {0: 48156, 5: 42526, 10: 9318}, + ("Category Fives", 2, 1): {0: 69299, 5: 30701}, + ("Category Fives", 2, 2): {0: 48156, 5: 51844}, ("Category Fives", 2, 3): {0: 33225, 5: 49153, 10: 17622}, ("Category Fives", 2, 4): {0: 23218, 5: 50075, 10: 26707}, ("Category Fives", 2, 5): {0: 15939, 5: 48313, 10: 35748}, @@ -359,59 +359,59 @@ ("Category Fives", 2, 7): {0: 7822, 5: 40388, 10: 51790}, ("Category Fives", 2, 8): {0: 5386, 5: 35636, 10: 58978}, ("Category Fives", 3, 0): {0: 100000}, - ("Category Fives", 3, 1): {0: 58034, 5: 34541, 10: 7425}, - ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 19403, 15: 2904}, - ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 30794, 15: 7492}, + ("Category Fives", 3, 1): {0: 58034, 5: 41966}, + ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 22307}, + ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 38286}, ("Category Fives", 3, 4): {0: 11196, 5: 36192, 10: 38673, 15: 13939}, - ("Category Fives", 3, 5): {0: 6561, 5: 29163, 10: 43014, 15: 21262}, - ("Category Fives", 3, 6): {0: 3719, 5: 22181, 10: 44611, 15: 29489}, - ("Category Fives", 3, 7): {0: 2099, 5: 16817, 10: 43466, 15: 37618}, - ("Category Fives", 3, 8): {0: 1281, 5: 12473, 10: 40936, 15: 45310}, + ("Category Fives", 3, 5): {0: 6561, 10: 72177, 15: 21262}, + ("Category Fives", 3, 6): {0: 3719, 10: 66792, 15: 29489}, + ("Category Fives", 3, 7): {0: 2099, 10: 60283, 15: 37618}, + ("Category Fives", 3, 8): {0: 1281, 10: 53409, 15: 45310}, ("Category Fives", 4, 0): {0: 100000}, ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, - ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 27041, 15: 8893}, - ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 17250, 20: 3208}, - ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 26968, 20: 7218}, - ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, - ("Category Fives", 4, 6): {0: 1291, 5: 9959, 10: 29833, 15: 39417, 20: 19500}, - ("Category Fives", 4, 7): {0: 623, 5: 6231, 10: 24360, 15: 41779, 20: 27007}, - ("Category Fives", 4, 8): {0: 313, 10: 23001, 15: 41957, 20: 34729}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 15: 35934}, + ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 20458}, + ("Category Fives", 4, 4): {0: 5362, 10: 60452, 20: 34186}, + ("Category Fives", 4, 5): {0: 2655, 10: 50264, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 1291, 10: 39792, 15: 39417, 20: 19500}, + ("Category Fives", 4, 7): {0: 6854, 15: 66139, 20: 27007}, + ("Category Fives", 4, 8): {0: 4150, 15: 61121, 20: 34729}, ("Category Fives", 5, 0): {0: 100000}, - ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 16029, 15: 3499}, - ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 13793, 20: 3266}, - ("Category Fives", 5, 3): {0: 6526, 5: 23716, 10: 34430, 15: 25017, 20: 10311}, - ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 17219, 25: 3811}, - ("Category Fives", 5, 5): {0: 1063, 5: 7876, 10: 23203, 15: 34489, 20: 25757, 25: 7612}, - ("Category Fives", 5, 6): {0: 429, 5: 4091, 10: 16696, 15: 32855, 20: 32891, 25: 13038}, - ("Category Fives", 5, 7): {0: 159, 10: 13509, 15: 29416, 20: 37778, 25: 19138}, - ("Category Fives", 5, 8): {0: 1179, 10: 7453, 15: 24456, 20: 40615, 25: 26297}, + ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 19528}, + ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 17059}, + ("Category Fives", 5, 3): {0: 6526, 10: 58146, 20: 35328}, + ("Category Fives", 5, 4): {0: 2615, 10: 44108, 15: 32247, 20: 21030}, + ("Category Fives", 5, 5): {0: 1063, 10: 31079, 15: 34489, 25: 33369}, + ("Category Fives", 5, 6): {0: 4520, 15: 49551, 20: 32891, 25: 13038}, + ("Category Fives", 5, 7): {0: 2370, 15: 40714, 20: 37778, 25: 19138}, + ("Category Fives", 5, 8): {0: 1179, 15: 31909, 20: 40615, 25: 26297}, ("Category Fives", 6, 0): {0: 100000}, - ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 20181, 15: 6176}, - ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 19004, 20: 7397}, - ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 15759, 25: 5185}, - ("Category Fives", 6, 4): {0: 1201, 5: 8226, 10: 21518, 15: 31229, 20: 25160, 25: 12666}, - ("Category Fives", 6, 5): {0: 433, 10: 17879, 15: 27961, 20: 30800, 25: 18442, 30: 4485}, - ("Category Fives", 6, 6): {0: 141, 10: 10040, 15: 22226, 20: 32744, 25: 26341, 30: 8508}, - ("Category Fives", 6, 7): {0: 772, 10: 4724, 15: 16206, 20: 31363, 25: 32784, 30: 14151}, - ("Category Fives", 6, 8): {0: 297, 15: 13902, 20: 28004, 25: 37178, 30: 20619}, + ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 26357}, + ("Category Fives", 6, 2): {0: 11322, 10: 62277, 20: 26401}, + ("Category Fives", 6, 3): {0: 3765, 10: 46058, 20: 50177}, + ("Category Fives", 6, 4): {0: 1201, 15: 60973, 25: 37826}, + ("Category Fives", 6, 5): {0: 4307, 15: 41966, 20: 30800, 25: 22927}, + ("Category Fives", 6, 6): {0: 1827, 15: 30580, 20: 32744, 30: 34849}, + ("Category Fives", 6, 7): {0: 5496, 20: 47569, 25: 32784, 30: 14151}, + ("Category Fives", 6, 8): {0: 2920, 20: 39283, 25: 37178, 30: 20619}, ("Category Fives", 7, 0): {0: 100000}, - ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 23567, 15: 9453}, - ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 10140, 25: 3122}, - ("Category Fives", 7, 3): {0: 2262, 5: 11013, 10: 24443, 15: 29307, 20: 21387, 25: 11588}, - ("Category Fives", 7, 4): {0: 618, 5: 4583, 10: 14761, 15: 26159, 20: 28335, 25: 18050, 30: 7494}, - ("Category Fives", 7, 5): {0: 183, 10: 9616, 15: 19685, 20: 28915, 25: 26000, 30: 12883, 35: 2718}, - ("Category Fives", 7, 6): {0: 670, 15: 16878, 20: 25572, 25: 30456, 30: 20695, 35: 5729}, - ("Category Fives", 7, 7): {0: 255, 15: 9718, 20: 20696, 25: 31883, 30: 27333, 35: 10115}, - ("Category Fives", 7, 8): {0: 927, 15: 4700, 20: 15292, 25: 30298, 30: 33015, 35: 15768}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 15: 33020}, + ("Category Fives", 7, 2): {0: 7609, 10: 55915, 20: 36476}, + ("Category Fives", 7, 3): {0: 2262, 10: 35456, 20: 62282}, + ("Category Fives", 7, 4): {0: 5201, 15: 40920, 25: 53879}, + ("Category Fives", 7, 5): {0: 1890, 20: 56509, 30: 41601}, + ("Category Fives", 7, 6): {0: 4506, 20: 38614, 25: 30456, 30: 26424}, + ("Category Fives", 7, 7): {0: 2107, 25: 60445, 35: 37448}, + ("Category Fives", 7, 8): {0: 5627, 25: 45590, 30: 33015, 35: 15768}, ("Category Fives", 8, 0): {0: 100000}, - ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 10392, 20: 3069}, - ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 14056, 25: 6230}, - ("Category Fives", 8, 3): {0: 1258, 5: 7317, 10: 18783, 15: 27375, 20: 24542, 25: 14322, 30: 6403}, - ("Category Fives", 8, 4): {0: 271, 10: 11864, 15: 20267, 20: 27158, 25: 23589, 30: 12529, 35: 4322}, - ("Category Fives", 8, 5): {0: 943, 10: 4260, 15: 12456, 20: 23115, 25: 27968, 30: 20704, 35: 10554}, - ("Category Fives", 8, 6): {0: 281, 15: 8625, 20: 17201, 25: 27484, 30: 27178, 35: 15414, 40: 3817}, - ("Category Fives", 8, 7): {0: 746, 20: 14964, 25: 23717, 30: 30426, 35: 22677, 40: 7470}, - ("Category Fives", 8, 8): {0: 261, 20: 8927, 25: 18714, 30: 31084, 35: 29126, 40: 11888}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 15: 39408}, + ("Category Fives", 8, 2): {0: 5425, 10: 48295, 20: 46280}, + ("Category Fives", 8, 3): {0: 1258, 15: 53475, 25: 45267}, + ("Category Fives", 8, 4): {0: 2752, 20: 56808, 30: 40440}, + ("Category Fives", 8, 5): {0: 5203, 20: 35571, 30: 59226}, + ("Category Fives", 8, 6): {0: 1970, 25: 51621, 35: 46409}, + ("Category Fives", 8, 7): {0: 4281, 25: 35146, 30: 30426, 40: 30147}, + ("Category Fives", 8, 8): {0: 2040, 30: 56946, 40: 41014}, ("Category Sixes", 0, 0): {0: 100000}, ("Category Sixes", 0, 1): {0: 100000}, ("Category Sixes", 0, 2): {0: 100000}, @@ -431,8 +431,8 @@ ("Category Sixes", 1, 7): {0: 28251, 6: 71749}, ("Category Sixes", 1, 8): {0: 23206, 6: 76794}, ("Category Sixes", 2, 0): {0: 100000}, - ("Category Sixes", 2, 1): {0: 69463, 6: 27651, 12: 2886}, - ("Category Sixes", 2, 2): {0: 47896, 6: 42794, 12: 9310}, + ("Category Sixes", 2, 1): {0: 69463, 6: 30537}, + ("Category Sixes", 2, 2): {0: 47896, 6: 52104}, ("Category Sixes", 2, 3): {0: 33394, 6: 48757, 12: 17849}, ("Category Sixes", 2, 4): {0: 23552, 6: 49554, 12: 26894}, ("Category Sixes", 2, 5): {0: 16090, 6: 48098, 12: 35812}, @@ -440,59 +440,59 @@ ("Category Sixes", 2, 7): {0: 7737, 6: 40480, 12: 51783}, ("Category Sixes", 2, 8): {0: 5379, 6: 35672, 12: 58949}, ("Category Sixes", 3, 0): {0: 100000}, - ("Category Sixes", 3, 1): {0: 57718, 6: 34818, 12: 7464}, - ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 19159, 18: 2903}, - ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 30952, 18: 7436}, + ("Category Sixes", 3, 1): {0: 57718, 6: 42282}, + ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 22062}, + ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 38388}, ("Category Sixes", 3, 4): {0: 11144, 6: 36281, 12: 38817, 18: 13758}, ("Category Sixes", 3, 5): {0: 6414, 6: 28891, 12: 43114, 18: 21581}, - ("Category Sixes", 3, 6): {0: 3870, 6: 22394, 12: 44318, 18: 29418}, - ("Category Sixes", 3, 7): {0: 2188, 6: 16803, 12: 43487, 18: 37522}, - ("Category Sixes", 3, 8): {0: 1289, 6: 12421, 12: 41082, 18: 45208}, + ("Category Sixes", 3, 6): {0: 3870, 12: 66712, 18: 29418}, + ("Category Sixes", 3, 7): {0: 2188, 12: 60290, 18: 37522}, + ("Category Sixes", 3, 8): {0: 1289, 12: 53503, 18: 45208}, ("Category Sixes", 4, 0): {0: 100000}, ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, - ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 26935, 18: 8731}, - ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 17390, 24: 3157}, - ("Category Sixes", 4, 4): {0: 5324, 6: 23265, 12: 37209, 18: 26929, 24: 7273}, - ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, - ("Category Sixes", 4, 6): {0: 1282, 6: 9997, 12: 29855, 18: 39379, 24: 19487}, - ("Category Sixes", 4, 7): {0: 588, 6: 6202, 12: 24396, 18: 41935, 24: 26879}, - ("Category Sixes", 4, 8): {0: 317, 6: 3863, 12: 19042, 18: 42180, 24: 34598}, + ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 35666}, + ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 20547}, + ("Category Sixes", 4, 4): {0: 5324, 12: 60474, 18: 34202}, + ("Category Sixes", 4, 5): {0: 2658, 12: 50173, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 1282, 12: 39852, 18: 39379, 24: 19487}, + ("Category Sixes", 4, 7): {0: 588, 12: 30598, 18: 41935, 24: 26879}, + ("Category Sixes", 4, 8): {0: 4180, 18: 61222, 24: 34598}, ("Category Sixes", 5, 0): {0: 100000}, - ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 16206, 18: 3497}, - ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 13612, 24: 3281}, - ("Category Sixes", 5, 3): {0: 6456, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, - ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 17115, 30: 3771}, - ("Category Sixes", 5, 5): {0: 1119, 6: 7775, 12: 23063, 18: 34716, 24: 25568, 30: 7759}, - ("Category Sixes", 5, 6): {0: 392, 6: 4171, 12: 16724, 18: 32792, 24: 32829, 30: 13092}, - ("Category Sixes", 5, 7): {0: 197, 12: 13627, 18: 29190, 24: 37560, 30: 19426}, - ("Category Sixes", 5, 8): {0: 1246, 12: 7404, 18: 24560, 24: 40134, 30: 26656}, + ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 19703}, + ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 16893}, + ("Category Sixes", 5, 3): {0: 6456, 12: 58124, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 12: 44335, 18: 32198, 24: 20886}, + ("Category Sixes", 5, 5): {0: 1119, 12: 30838, 18: 34716, 24: 33327}, + ("Category Sixes", 5, 6): {0: 4563, 18: 49516, 24: 32829, 30: 13092}, + ("Category Sixes", 5, 7): {0: 2315, 18: 40699, 24: 37560, 30: 19426}, + ("Category Sixes", 5, 8): {0: 1246, 18: 31964, 24: 40134, 30: 26656}, ("Category Sixes", 6, 0): {0: 100000}, - ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 20198, 18: 6268}, - ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 19196, 24: 7514}, - ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 15863, 30: 5104}, - ("Category Sixes", 6, 4): {0: 1286, 6: 8066, 12: 21653, 18: 31264, 24: 25039, 30: 12692}, - ("Category Sixes", 6, 5): {0: 413, 6: 3777, 12: 13962, 18: 27705, 24: 30919, 30: 18670, 36: 4554}, - ("Category Sixes", 6, 6): {0: 146, 12: 10040, 18: 22320, 24: 32923, 30: 26086, 36: 8485}, - ("Category Sixes", 6, 7): {0: 814, 12: 4698, 18: 16286, 24: 31577, 30: 32487, 36: 14138}, - ("Category Sixes", 6, 8): {0: 328, 18: 14004, 24: 28064, 30: 37212, 36: 20392}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 18: 26466}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 24: 26710}, + ("Category Sixes", 6, 3): {0: 3787, 12: 46139, 18: 29107, 24: 20967}, + ("Category Sixes", 6, 4): {0: 1286, 12: 29719, 18: 31264, 24: 25039, 30: 12692}, + ("Category Sixes", 6, 5): {0: 4190, 18: 41667, 24: 30919, 30: 23224}, + ("Category Sixes", 6, 6): {0: 1804, 18: 30702, 24: 32923, 30: 34571}, + ("Category Sixes", 6, 7): {0: 51, 24: 53324, 30: 32487, 36: 14138}, + ("Category Sixes", 6, 8): {0: 2886, 24: 39510, 30: 37212, 36: 20392}, ("Category Sixes", 7, 0): {0: 100000}, - ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 23499, 18: 9665}, - ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 10316, 30: 3102}, - ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 9209, 36: 2529}, - ("Category Sixes", 7, 4): {0: 603, 6: 4459, 12: 14673, 18: 26303, 24: 28335, 30: 18228, 36: 7399}, - ("Category Sixes", 7, 5): {0: 172, 12: 9654, 18: 19381, 24: 29254, 30: 25790, 36: 12992, 42: 2757}, - ("Category Sixes", 7, 6): {0: 704, 12: 3864, 18: 13039, 24: 25760, 30: 30698, 36: 20143, 42: 5792}, - ("Category Sixes", 7, 7): {0: 257, 18: 9857, 24: 20557, 30: 31709, 36: 27546, 42: 10074}, - ("Category Sixes", 7, 8): {0: 872, 18: 4658, 24: 15419, 30: 30259, 36: 33183, 42: 15609}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 18: 33164}, + ("Category Sixes", 7, 2): {0: 7883, 12: 55404, 24: 36713}, + ("Category Sixes", 7, 3): {0: 2186, 12: 35249, 18: 29650, 30: 32915}, + ("Category Sixes", 7, 4): {0: 5062, 18: 40976, 24: 28335, 36: 25627}, + ("Category Sixes", 7, 5): {0: 1947, 18: 27260, 24: 29254, 30: 25790, 36: 15749}, + ("Category Sixes", 7, 6): {0: 4568, 24: 38799, 30: 30698, 42: 25935}, + ("Category Sixes", 7, 7): {0: 2081, 24: 28590, 30: 31709, 36: 37620}, + ("Category Sixes", 7, 8): {0: 73, 30: 51135, 36: 33183, 42: 15609}, ("Category Sixes", 8, 0): {0: 100000}, - ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 10483, 24: 3123}, - ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 14170, 30: 6166}, - ("Category Sixes", 8, 3): {0: 1246, 6: 7112, 12: 18757, 18: 27277, 24: 24802, 30: 14351, 36: 6455}, - ("Category Sixes", 8, 4): {0: 301, 12: 12044, 18: 20247, 24: 27146, 30: 23403, 36: 12524, 42: 4335}, - ("Category Sixes", 8, 5): {0: 859, 12: 4241, 18: 12477, 24: 23471, 30: 27655, 36: 20803, 42: 10494}, - ("Category Sixes", 8, 6): {0: 277, 18: 8656, 24: 17373, 30: 27347, 36: 27024, 42: 15394, 48: 3929}, - ("Category Sixes", 8, 7): {0: 766, 18: 3503, 24: 11451, 30: 23581, 36: 30772, 42: 22654, 48: 7273}, - ("Category Sixes", 8, 8): {6: 262, 24: 8866, 30: 18755, 36: 31116, 42: 28870, 48: 12131}, + ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 13606}, + ("Category Sixes", 8, 2): {0: 5280, 12: 48607, 18: 25777, 30: 20336}, + ("Category Sixes", 8, 3): {0: 1246, 12: 25869, 18: 27277, 30: 45608}, + ("Category Sixes", 8, 4): {0: 2761, 18: 29831, 24: 27146, 36: 40262}, + ("Category Sixes", 8, 5): {0: 5100, 24: 35948, 30: 27655, 42: 31297}, + ("Category Sixes", 8, 6): {0: 2067, 30: 51586, 36: 27024, 42: 19323}, + ("Category Sixes", 8, 7): {0: 4269, 30: 35032, 36: 30772, 48: 29927}, + ("Category Sixes", 8, 8): {6: 2012, 30: 25871, 36: 31116, 42: 28870, 48: 12131}, ("Category Choice", 0, 0): {0: 100000}, ("Category Choice", 0, 1): {0: 100000}, ("Category Choice", 0, 2): {0: 100000}, @@ -503,77 +503,77 @@ ("Category Choice", 0, 7): {0: 100000}, ("Category Choice", 0, 8): {0: 100000}, ("Category Choice", 1, 0): {0: 100000}, - ("Category Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Choice", 2, 0): {0: 100000}, - ("Category Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Choice", 3, 0): {0: 100000}, - ("Category Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Choice", 4, 0): {0: 100000}, - ("Category Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Choice", 5, 0): {0: 100000}, - ("Category Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Choice", 6, 0): {0: 100000}, - ("Category Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Choice", 6, 2): {8: 1504, 16: 8676, 20: 10032, 22: 14673, 26: 27312, 27: 16609, 29: 12133, 31: 9061}, - ("Category Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Choice", 7, 0): {0: 100000}, - ("Category Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Choice", 8, 0): {0: 100000}, - ("Category Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Choice", 8, 2): {11: 5900, 26: 10331, 29: 11435, 31: 14533, 34: 23939, 36: 13855, 38: 10165, 40: 9842}, - ("Category Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Choice", 8, 5): {16: 4986, 32: 9031, 35: 10214, 37: 14528, 41: 25608, 42: 16131, 44: 11245, 46: 8257}, - ("Category Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Inverse Choice", 0, 0): {0: 100000}, ("Category Inverse Choice", 0, 1): {0: 100000}, ("Category Inverse Choice", 0, 2): {0: 100000}, @@ -584,104 +584,77 @@ ("Category Inverse Choice", 0, 7): {0: 100000}, ("Category Inverse Choice", 0, 8): {0: 100000}, ("Category Inverse Choice", 1, 0): {0: 100000}, - ("Category Inverse Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Inverse Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Inverse Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Inverse Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Inverse Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Inverse Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Inverse Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Inverse Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Inverse Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Inverse Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Inverse Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Inverse Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Inverse Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Inverse Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Inverse Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Inverse Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Inverse Choice", 2, 0): {0: 100000}, - ("Category Inverse Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Inverse Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Inverse Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Inverse Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Inverse Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Inverse Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Inverse Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Inverse Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Inverse Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Inverse Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Inverse Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Inverse Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Inverse Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Inverse Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Inverse Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Inverse Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Inverse Choice", 3, 0): {0: 100000}, - ("Category Inverse Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Inverse Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Inverse Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Inverse Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Inverse Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Inverse Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Inverse Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Inverse Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Inverse Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Inverse Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Inverse Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Inverse Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Inverse Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Inverse Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Inverse Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Inverse Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Inverse Choice", 4, 0): {0: 100000}, - ("Category Inverse Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Inverse Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Inverse Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Inverse Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Inverse Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Inverse Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Inverse Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Inverse Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Inverse Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Inverse Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Inverse Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Inverse Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Inverse Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Inverse Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Inverse Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Inverse Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Inverse Choice", 5, 0): {0: 100000}, - ("Category Inverse Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Inverse Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Inverse Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Inverse Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Inverse Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Inverse Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Inverse Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Inverse Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Inverse Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Inverse Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Inverse Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Inverse Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Inverse Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Inverse Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Inverse Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Inverse Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Inverse Choice", 6, 0): {0: 100000}, - ("Category Inverse Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Inverse Choice", 6, 2): { - 8: 1504, - 16: 8676, - 20: 10032, - 22: 14673, - 26: 27312, - 27: 16609, - 29: 12133, - 31: 9061, - }, - ("Category Inverse Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Inverse Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Inverse Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Inverse Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Inverse Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Inverse Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Inverse Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Inverse Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Inverse Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Inverse Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Inverse Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Inverse Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Inverse Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Inverse Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Inverse Choice", 7, 0): {0: 100000}, - ("Category Inverse Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Inverse Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Inverse Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Inverse Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Inverse Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Inverse Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Inverse Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Inverse Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Inverse Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Inverse Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Inverse Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Inverse Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Inverse Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Inverse Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Inverse Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Inverse Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Inverse Choice", 8, 0): {0: 100000}, - ("Category Inverse Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Inverse Choice", 8, 2): { - 11: 5900, - 26: 10331, - 29: 11435, - 31: 14533, - 34: 23939, - 36: 13855, - 38: 10165, - 40: 9842, - }, - ("Category Inverse Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Inverse Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Inverse Choice", 8, 5): { - 16: 4986, - 32: 9031, - 35: 10214, - 37: 14528, - 41: 25608, - 42: 16131, - 44: 11245, - 46: 8257, - }, - ("Category Inverse Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Inverse Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Inverse Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Inverse Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Inverse Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Inverse Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Inverse Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Inverse Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Inverse Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Inverse Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Inverse Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Pair", 0, 0): {0: 100000}, ("Category Pair", 0, 1): {0: 100000}, ("Category Pair", 0, 2): {0: 100000}, @@ -791,7 +764,7 @@ ("Category Three of a Kind", 2, 7): {0: 100000}, ("Category Three of a Kind", 2, 8): {0: 100000}, ("Category Three of a Kind", 3, 0): {0: 100000}, - ("Category Three of a Kind", 3, 1): {0: 97222, 20: 2778}, + ("Category Three of a Kind", 3, 1): {0: 100000}, ("Category Three of a Kind", 3, 2): {0: 88880, 20: 11120}, ("Category Three of a Kind", 3, 3): {0: 78187, 20: 21813}, ("Category Three of a Kind", 3, 4): {0: 67476, 20: 32524}, @@ -881,7 +854,7 @@ ("Category Four of a Kind", 3, 7): {0: 100000}, ("Category Four of a Kind", 3, 8): {0: 100000}, ("Category Four of a Kind", 4, 0): {0: 100000}, - ("Category Four of a Kind", 4, 1): {0: 99516, 30: 484}, + ("Category Four of a Kind", 4, 1): {0: 100000}, ("Category Four of a Kind", 4, 2): {0: 96122, 30: 3878}, ("Category Four of a Kind", 4, 3): {0: 89867, 30: 10133}, ("Category Four of a Kind", 4, 4): {0: 81771, 30: 18229}, @@ -1304,7 +1277,7 @@ ("Category Yacht", 5, 7): {0: 67007, 50: 32993}, ("Category Yacht", 5, 8): {0: 58618, 50: 41382}, ("Category Yacht", 6, 0): {0: 100000}, - ("Category Yacht", 6, 1): {0: 99571, 50: 429}, + ("Category Yacht", 6, 1): {0: 100000}, ("Category Yacht", 6, 2): {0: 94726, 50: 5274}, ("Category Yacht", 6, 3): {0: 84366, 50: 15634}, ("Category Yacht", 6, 4): {0: 70782, 50: 29218}, @@ -1313,7 +1286,7 @@ ("Category Yacht", 6, 7): {0: 33578, 50: 66422}, ("Category Yacht", 6, 8): {0: 25079, 50: 74921}, ("Category Yacht", 7, 0): {0: 100000}, - ("Category Yacht", 7, 1): {0: 98833, 50: 1167}, + ("Category Yacht", 7, 1): {0: 100000}, ("Category Yacht", 7, 2): {0: 87511, 50: 12489}, ("Category Yacht", 7, 3): {0: 68252, 50: 31748}, ("Category Yacht", 7, 4): {0: 49065, 50: 50935}, @@ -1346,51 +1319,51 @@ ("Category Distincts", 2, 6): {1: 1, 2: 99999}, ("Category Distincts", 2, 7): {2: 100000}, ("Category Distincts", 2, 8): {2: 100000}, - ("Category Distincts", 3, 1): {1: 2760, 2: 41714, 3: 55526}, - ("Category Distincts", 3, 2): {1: 78, 3: 99922}, + ("Category Distincts", 3, 1): {1: 2760, 3: 97240}, + ("Category Distincts", 3, 2): {1: 15014, 3: 84986}, ("Category Distincts", 3, 3): {1: 4866, 3: 95134}, ("Category Distincts", 3, 4): {2: 1659, 3: 98341}, ("Category Distincts", 3, 5): {2: 575, 3: 99425}, ("Category Distincts", 3, 6): {2: 200, 3: 99800}, ("Category Distincts", 3, 7): {2: 69, 3: 99931}, ("Category Distincts", 3, 8): {2: 22, 3: 99978}, - ("Category Distincts", 4, 1): {1: 494, 3: 71611, 4: 27895}, - ("Category Distincts", 4, 2): {1: 1893, 3: 36922, 4: 61185}, - ("Category Distincts", 4, 3): {2: 230, 4: 99770}, - ("Category Distincts", 4, 4): {2: 21, 4: 99979}, + ("Category Distincts", 4, 1): {1: 16634, 3: 83366}, + ("Category Distincts", 4, 2): {1: 1893, 4: 98107}, + ("Category Distincts", 4, 3): {2: 19861, 4: 80139}, + ("Category Distincts", 4, 4): {2: 9879, 4: 90121}, ("Category Distincts", 4, 5): {2: 4906, 4: 95094}, ("Category Distincts", 4, 6): {3: 2494, 4: 97506}, ("Category Distincts", 4, 7): {3: 1297, 4: 98703}, ("Category Distincts", 4, 8): {3: 611, 4: 99389}, - ("Category Distincts", 5, 1): {1: 5798, 3: 38538, 4: 55664}, - ("Category Distincts", 5, 2): {2: 196, 4: 68119, 5: 31685}, - ("Category Distincts", 5, 3): {2: 3022, 4: 44724, 5: 52254}, - ("Category Distincts", 5, 4): {3: 722, 4: 31632, 5: 67646}, - ("Category Distincts", 5, 5): {3: 215, 4: 21391, 5: 78394}, - ("Category Distincts", 5, 6): {3: 55, 5: 99945}, - ("Category Distincts", 5, 7): {3: 15, 5: 99985}, + ("Category Distincts", 5, 1): {1: 5798, 4: 94202}, + ("Category Distincts", 5, 2): {2: 11843, 4: 88157}, + ("Category Distincts", 5, 3): {2: 3022, 5: 96978}, + ("Category Distincts", 5, 4): {3: 32354, 5: 67646}, + ("Category Distincts", 5, 5): {3: 21606, 5: 78394}, + ("Category Distincts", 5, 6): {3: 14525, 5: 85475}, + ("Category Distincts", 5, 7): {3: 9660, 5: 90340}, ("Category Distincts", 5, 8): {3: 6463, 5: 93537}, - ("Category Distincts", 6, 1): {1: 2027, 3: 22985, 4: 50464, 5: 24524}, - ("Category Distincts", 6, 2): {2: 3299, 4: 35174, 5: 61527}, - ("Category Distincts", 6, 3): {3: 417, 5: 79954, 6: 19629}, - ("Category Distincts", 6, 4): {3: 7831, 5: 61029, 6: 31140}, - ("Category Distincts", 6, 5): {3: 3699, 5: 54997, 6: 41304}, - ("Category Distincts", 6, 6): {4: 1557, 5: 47225, 6: 51218}, - ("Category Distincts", 6, 7): {4: 728, 5: 40465, 6: 58807}, - ("Category Distincts", 6, 8): {4: 321, 5: 33851, 6: 65828}, - ("Category Distincts", 7, 1): {1: 665, 4: 57970, 5: 41365}, - ("Category Distincts", 7, 2): {2: 839, 5: 75578, 6: 23583}, - ("Category Distincts", 7, 3): {3: 6051, 5: 50312, 6: 43637}, - ("Category Distincts", 7, 4): {3: 1796, 5: 38393, 6: 59811}, - ("Category Distincts", 7, 5): {4: 529, 5: 27728, 6: 71743}, - ("Category Distincts", 7, 6): {4: 164, 6: 99836}, - ("Category Distincts", 7, 7): {4: 53, 6: 99947}, - ("Category Distincts", 7, 8): {4: 14, 6: 99986}, - ("Category Distincts", 8, 1): {1: 7137, 4: 36582, 5: 56281}, - ("Category Distincts", 8, 2): {2: 233, 5: 59964, 6: 39803}, - ("Category Distincts", 8, 3): {3: 1976, 5: 34748, 6: 63276}, - ("Category Distincts", 8, 4): {4: 389, 5: 21008, 6: 78603}, - ("Category Distincts", 8, 5): {4: 78, 6: 99922}, + ("Category Distincts", 6, 1): {1: 25012, 4: 74988}, + ("Category Distincts", 6, 2): {2: 3299, 5: 96701}, + ("Category Distincts", 6, 3): {3: 17793, 5: 82207}, + ("Category Distincts", 6, 4): {3: 7831, 5: 92169}, + ("Category Distincts", 6, 5): {3: 3699, 6: 96301}, + ("Category Distincts", 6, 6): {4: 1557, 6: 98443}, + ("Category Distincts", 6, 7): {4: 728, 6: 99272}, + ("Category Distincts", 6, 8): {4: 321, 6: 99679}, + ("Category Distincts", 7, 1): {1: 13671, 5: 86329}, + ("Category Distincts", 7, 2): {2: 19686, 5: 80314}, + ("Category Distincts", 7, 3): {3: 6051, 6: 93949}, + ("Category Distincts", 7, 4): {3: 1796, 6: 98204}, + ("Category Distincts", 7, 5): {4: 28257, 6: 71743}, + ("Category Distincts", 7, 6): {4: 19581, 6: 80419}, + ("Category Distincts", 7, 7): {4: 13618, 6: 86382}, + ("Category Distincts", 7, 8): {4: 9545, 6: 90455}, + ("Category Distincts", 8, 1): {1: 7137, 5: 92863}, + ("Category Distincts", 8, 2): {2: 9414, 6: 90586}, + ("Category Distincts", 8, 3): {3: 1976, 6: 98024}, + ("Category Distincts", 8, 4): {4: 21397, 6: 78603}, + ("Category Distincts", 8, 5): {4: 12592, 6: 87408}, ("Category Distincts", 8, 6): {4: 7177, 6: 92823}, ("Category Distincts", 8, 7): {4: 4179, 6: 95821}, ("Category Distincts", 8, 8): {5: 2440, 6: 97560}, @@ -1404,8 +1377,8 @@ ("Category Two times Ones", 0, 7): {0: 100000}, ("Category Two times Ones", 0, 8): {0: 100000}, ("Category Two times Ones", 1, 0): {0: 100000}, - ("Category Two times Ones", 1, 1): {0: 83475, 2: 16525}, - ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, + ("Category Two times Ones", 1, 1): {0: 100000}, + ("Category Two times Ones", 1, 2): {0: 100000}, ("Category Two times Ones", 1, 3): {0: 57818, 2: 42182}, ("Category Two times Ones", 1, 4): {0: 48418, 2: 51582}, ("Category Two times Ones", 1, 5): {0: 40301, 2: 59699}, @@ -1413,68 +1386,68 @@ ("Category Two times Ones", 1, 7): {0: 28182, 2: 71818}, ("Category Two times Ones", 1, 8): {0: 23406, 2: 76594}, ("Category Two times Ones", 2, 0): {0: 100000}, - ("Category Two times Ones", 2, 1): {0: 69724, 2: 30276}, - ("Category Two times Ones", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Two times Ones", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Two times Ones", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Two times Ones", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Two times Ones", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Two times Ones", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Two times Ones", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Two times Ones", 2, 1): {0: 100000}, + ("Category Two times Ones", 2, 2): {0: 48238, 2: 51762}, + ("Category Two times Ones", 2, 3): {0: 33290, 4: 66710}, + ("Category Two times Ones", 2, 4): {0: 23136, 4: 76864}, + ("Category Two times Ones", 2, 5): {0: 16146, 4: 83854}, + ("Category Two times Ones", 2, 6): {0: 11083, 4: 88917}, + ("Category Two times Ones", 2, 7): {0: 7662, 4: 92338}, + ("Category Two times Ones", 2, 8): {0: 5354, 4: 94646}, ("Category Two times Ones", 3, 0): {0: 100000}, - ("Category Two times Ones", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Two times Ones", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Two times Ones", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Two times Ones", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Two times Ones", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Two times Ones", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Two times Ones", 3, 1): {0: 58021, 2: 41979}, + ("Category Two times Ones", 3, 2): {0: 33548, 4: 66452}, + ("Category Two times Ones", 3, 3): {0: 19375, 4: 80625}, + ("Category Two times Ones", 3, 4): {0: 10998, 4: 89002}, + ("Category Two times Ones", 3, 5): {0: 6519, 6: 93481}, + ("Category Two times Ones", 3, 6): {0: 3619, 6: 96381}, + ("Category Two times Ones", 3, 7): {0: 2195, 6: 97805}, + ("Category Two times Ones", 3, 8): {0: 13675, 6: 86325}, ("Category Two times Ones", 4, 0): {0: 100000}, - ("Category Two times Ones", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Two times Ones", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Two times Ones", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Two times Ones", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Two times Ones", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Two times Ones", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Two times Ones", 4, 1): {0: 48235, 2: 51765}, + ("Category Two times Ones", 4, 2): {0: 23289, 4: 76711}, + ("Category Two times Ones", 4, 3): {0: 11177, 6: 88823}, + ("Category Two times Ones", 4, 4): {0: 5499, 6: 94501}, + ("Category Two times Ones", 4, 5): {0: 18356, 6: 81644}, + ("Category Two times Ones", 4, 6): {0: 11169, 8: 88831}, + ("Category Two times Ones", 4, 7): {0: 6945, 8: 93055}, + ("Category Two times Ones", 4, 8): {0: 4091, 8: 95909}, ("Category Two times Ones", 5, 0): {0: 100000}, - ("Category Two times Ones", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Two times Ones", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Two times Ones", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Two times Ones", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Two times Ones", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Two times Ones", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Two times Ones", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Two times Ones", 5, 1): {0: 40028, 4: 59972}, + ("Category Two times Ones", 5, 2): {0: 16009, 6: 83991}, + ("Category Two times Ones", 5, 3): {0: 6489, 6: 93511}, + ("Category Two times Ones", 5, 4): {0: 16690, 8: 83310}, + ("Category Two times Ones", 5, 5): {0: 9016, 8: 90984}, + ("Category Two times Ones", 5, 6): {0: 4602, 8: 95398}, + ("Category Two times Ones", 5, 7): {0: 13627, 10: 86373}, + ("Category Two times Ones", 5, 8): {0: 8742, 10: 91258}, ("Category Two times Ones", 6, 0): {0: 100000}, - ("Category Two times Ones", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Two times Ones", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Two times Ones", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Two times Ones", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Two times Ones", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Two times Ones", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Two times Ones", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Two times Ones", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Two times Ones", 6, 1): {0: 33502, 4: 66498}, + ("Category Two times Ones", 6, 2): {0: 11210, 6: 88790}, + ("Category Two times Ones", 6, 3): {0: 3673, 6: 96327}, + ("Category Two times Ones", 6, 4): {0: 9291, 8: 90709}, + ("Category Two times Ones", 6, 5): {0: 441, 8: 99559}, + ("Category Two times Ones", 6, 6): {0: 10255, 10: 89745}, + ("Category Two times Ones", 6, 7): {0: 5646, 10: 94354}, + ("Category Two times Ones", 6, 8): {0: 14287, 12: 85713}, ("Category Two times Ones", 7, 0): {0: 100000}, - ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Two times Ones", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Two times Ones", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Two times Ones", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Two times Ones", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Two times Ones", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Two times Ones", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Two times Ones", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Two times Ones", 7, 1): {0: 27683, 4: 72317}, + ("Category Two times Ones", 7, 2): {0: 7824, 6: 92176}, + ("Category Two times Ones", 7, 3): {0: 13167, 8: 86833}, + ("Category Two times Ones", 7, 4): {0: 564, 10: 99436}, + ("Category Two times Ones", 7, 5): {0: 9824, 10: 90176}, + ("Category Two times Ones", 7, 6): {0: 702, 12: 99298}, + ("Category Two times Ones", 7, 7): {0: 10186, 12: 89814}, + ("Category Two times Ones", 7, 8): {0: 942, 12: 99058}, ("Category Two times Ones", 8, 0): {0: 100000}, - ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Two times Ones", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Two times Ones", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Two times Ones", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Two times Ones", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Two times Ones", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Two times Ones", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Two times Ones", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Two times Ones", 8, 1): {0: 23378, 4: 76622}, + ("Category Two times Ones", 8, 2): {0: 5420, 8: 94580}, + ("Category Two times Ones", 8, 3): {0: 8560, 10: 91440}, + ("Category Two times Ones", 8, 4): {0: 12199, 12: 87801}, + ("Category Two times Ones", 8, 5): {0: 879, 12: 99121}, + ("Category Two times Ones", 8, 6): {0: 9033, 14: 90967}, + ("Category Two times Ones", 8, 7): {0: 15767, 14: 84233}, + ("Category Two times Ones", 8, 8): {2: 9033, 14: 90967}, ("Category Half of Sixes", 0, 0): {0: 100000}, ("Category Half of Sixes", 0, 1): {0: 100000}, ("Category Half of Sixes", 0, 2): {0: 100000}, @@ -1485,7 +1458,7 @@ ("Category Half of Sixes", 0, 7): {0: 100000}, ("Category Half of Sixes", 0, 8): {0: 100000}, ("Category Half of Sixes", 1, 0): {0: 100000}, - ("Category Half of Sixes", 1, 1): {0: 83343, 3: 16657}, + ("Category Half of Sixes", 1, 1): {0: 100000}, ("Category Half of Sixes", 1, 2): {0: 69569, 3: 30431}, ("Category Half of Sixes", 1, 3): {0: 57872, 3: 42128}, ("Category Half of Sixes", 1, 4): {0: 48081, 3: 51919}, @@ -1495,1558 +1468,387 @@ ("Category Half of Sixes", 1, 8): {0: 23240, 3: 76760}, ("Category Half of Sixes", 2, 0): {0: 100000}, ("Category Half of Sixes", 2, 1): {0: 69419, 3: 30581}, - ("Category Half of Sixes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Half of Sixes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Half of Sixes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Half of Sixes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Half of Sixes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Half of Sixes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Half of Sixes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Half of Sixes", 2, 2): {0: 48202, 3: 51798}, + ("Category Half of Sixes", 2, 3): {0: 33376, 6: 66624}, + ("Category Half of Sixes", 2, 4): {0: 23276, 6: 76724}, + ("Category Half of Sixes", 2, 5): {0: 16092, 6: 83908}, + ("Category Half of Sixes", 2, 6): {0: 11232, 6: 88768}, + ("Category Half of Sixes", 2, 7): {0: 7589, 6: 92411}, + ("Category Half of Sixes", 2, 8): {0: 5447, 6: 94553}, ("Category Half of Sixes", 3, 0): {0: 100000}, - ("Category Half of Sixes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Half of Sixes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Half of Sixes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Half of Sixes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Half of Sixes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Half of Sixes", 3, 1): {0: 57964, 3: 42036}, + ("Category Half of Sixes", 3, 2): {0: 33637, 6: 66363}, + ("Category Half of Sixes", 3, 3): {0: 19520, 6: 80480}, + ("Category Half of Sixes", 3, 4): {0: 11265, 6: 88735}, + ("Category Half of Sixes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Half of Sixes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Half of Sixes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Half of Sixes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Half of Sixes", 4, 0): {0: 100000}, - ("Category Half of Sixes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Half of Sixes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Half of Sixes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Half of Sixes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Half of Sixes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Half of Sixes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Half of Sixes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Half of Sixes", 4, 1): {0: 48121, 6: 51879}, + ("Category Half of Sixes", 4, 2): {0: 23296, 6: 76704}, + ("Category Half of Sixes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Half of Sixes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Half of Sixes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Half of Sixes", 4, 6): {0: 11267, 9: 88733}, + ("Category Half of Sixes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Half of Sixes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Half of Sixes", 5, 0): {0: 100000}, - ("Category Half of Sixes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Half of Sixes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Half of Sixes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Half of Sixes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Half of Sixes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Half of Sixes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Half of Sixes", 5, 1): {0: 40183, 6: 59817}, + ("Category Half of Sixes", 5, 2): {0: 16197, 6: 83803}, + ("Category Half of Sixes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Half of Sixes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Half of Sixes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Half of Sixes", 5, 6): {0: 4652, 12: 95348}, + ("Category Half of Sixes", 5, 7): {0: 2365, 12: 97635}, + ("Category Half of Sixes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Half of Sixes", 6, 0): {0: 100000}, - ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Half of Sixes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Half of Sixes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Half of Sixes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Half of Sixes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Half of Sixes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Half of Sixes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Half of Sixes", 6, 1): {0: 33473, 6: 66527}, + ("Category Half of Sixes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Half of Sixes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Half of Sixes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Half of Sixes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Half of Sixes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Half of Sixes", 6, 7): {0: 5519, 15: 94481}, + ("Category Half of Sixes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Half of Sixes", 7, 0): {0: 100000}, - ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Half of Sixes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Half of Sixes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Half of Sixes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Half of Sixes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Half of Sixes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Half of Sixes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Half of Sixes", 7, 1): {0: 27933, 6: 72067}, + ("Category Half of Sixes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Half of Sixes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Half of Sixes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Half of Sixes", 7, 5): {0: 9894, 15: 90106}, + ("Category Half of Sixes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Half of Sixes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Half of Sixes", 7, 8): {0: 5710, 18: 94290}, ("Category Half of Sixes", 8, 0): {0: 100000}, - ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Half of Sixes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Half of Sixes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Half of Sixes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Half of Sixes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Half of Sixes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Half of Sixes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Half of Sixes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, - ("Category Twos and Threes", 1, 1): {0: 66466, 3: 33534}, - ("Category Twos and Threes", 1, 2): {0: 55640, 3: 44360}, - ("Category Twos and Threes", 1, 3): {0: 46223, 3: 53777}, - ("Category Twos and Threes", 1, 4): {0: 38552, 3: 61448}, - ("Category Twos and Threes", 1, 5): {0: 32320, 3: 67680}, - ("Category Twos and Threes", 1, 6): {0: 26733, 3: 73267}, - ("Category Twos and Threes", 1, 7): {0: 22289, 3: 77711}, - ("Category Twos and Threes", 1, 8): {0: 18676, 3: 81324}, - ("Category Twos and Threes", 2, 1): {0: 44565, 2: 21965, 3: 25172, 5: 8298}, - ("Category Twos and Threes", 2, 2): {0: 30855, 3: 51429, 6: 17716}, - ("Category Twos and Threes", 2, 3): {0: 21509, 3: 51178, 6: 27313}, - ("Category Twos and Threes", 2, 4): {0: 14935, 3: 48581, 6: 36484}, - ("Category Twos and Threes", 2, 5): {0: 10492, 3: 44256, 6: 45252}, - ("Category Twos and Threes", 2, 6): {0: 10775, 3: 35936, 6: 53289}, - ("Category Twos and Threes", 2, 7): {0: 7375, 3: 32469, 6: 60156}, - ("Category Twos and Threes", 2, 8): {0: 5212, 3: 35730, 6: 59058}, - ("Category Twos and Threes", 3, 1): {0: 29892, 2: 22136, 3: 27781, 6: 20191}, - ("Category Twos and Threes", 3, 2): {0: 17285, 3: 44257, 6: 38458}, - ("Category Twos and Threes", 3, 3): {0: 9889, 3: 36505, 6: 40112, 8: 13494}, - ("Category Twos and Threes", 3, 4): {0: 5717, 3: 28317, 6: 43044, 9: 22922}, - ("Category Twos and Threes", 3, 5): {0: 5795, 3: 19123, 6: 45004, 9: 30078}, - ("Category Twos and Threes", 3, 6): {0: 3273, 3: 21888, 6: 36387, 9: 38452}, - ("Category Twos and Threes", 3, 7): {0: 1917, 3: 16239, 6: 35604, 9: 46240}, - ("Category Twos and Threes", 3, 8): {0: 1124, 3: 12222, 6: 33537, 9: 53117}, - ("Category Twos and Threes", 4, 1): {0: 19619, 3: 46881, 6: 33500}, - ("Category Twos and Threes", 4, 2): {0: 9395, 3: 33926, 6: 37832, 9: 18847}, - ("Category Twos and Threes", 4, 3): {0: 4538, 3: 22968, 6: 38891, 9: 33603}, - ("Category Twos and Threes", 4, 4): {0: 4402, 3: 12654, 6: 35565, 9: 34784, 11: 12595}, - ("Category Twos and Threes", 4, 5): {0: 2065, 3: 14351, 6: 23592, 9: 38862, 12: 21130}, - ("Category Twos and Threes", 4, 6): {0: 1044, 3: 9056, 6: 20013, 9: 41255, 12: 28632}, - ("Category Twos and Threes", 4, 7): {0: 6310, 7: 24021, 9: 34297, 12: 35372}, - ("Category Twos and Threes", 4, 8): {0: 3694, 6: 18611, 9: 34441, 12: 43254}, - ("Category Twos and Threes", 5, 1): {0: 13070, 3: 33021, 5: 24568, 6: 16417, 8: 12924}, - ("Category Twos and Threes", 5, 2): {0: 5213, 3: 24275, 6: 37166, 9: 24746, 11: 8600}, - ("Category Twos and Threes", 5, 3): {0: 4707, 3: 10959, 6: 31388, 9: 33265, 12: 19681}, - ("Category Twos and Threes", 5, 4): {0: 1934, 3: 12081, 6: 17567, 9: 35282, 12: 33136}, - ("Category Twos and Threes", 5, 5): {0: 380, 2: 7025, 6: 13268, 9: 33274, 12: 33255, 14: 12798}, - ("Category Twos and Threes", 5, 6): {0: 3745, 6: 15675, 9: 22902, 12: 44665, 15: 13013}, - ("Category Twos and Threes", 5, 7): {0: 1969, 6: 10700, 9: 19759, 12: 39522, 15: 28050}, - ("Category Twos and Threes", 5, 8): {0: 13, 2: 7713, 10: 23957, 12: 32501, 15: 35816}, - ("Category Twos and Threes", 6, 1): {0: 8955, 3: 26347, 5: 24850, 8: 39848}, - ("Category Twos and Threes", 6, 2): {0: 2944, 3: 16894, 6: 32156, 9: 37468, 12: 10538}, - ("Category Twos and Threes", 6, 3): {0: 2484, 3: 13120, 6: 15999, 9: 32271, 12: 24898, 14: 11228}, - ("Category Twos and Threes", 6, 4): {0: 320, 2: 6913, 6: 10814, 9: 28622, 12: 31337, 15: 21994}, - ("Category Twos and Threes", 6, 5): {0: 3135, 6: 12202, 9: 16495, 12: 33605, 15: 26330, 17: 8233}, - ("Category Twos and Threes", 6, 6): {0: 98, 3: 8409, 9: 12670, 12: 31959, 15: 38296, 18: 8568}, - ("Category Twos and Threes", 6, 7): {0: 4645, 9: 15210, 12: 21906, 15: 44121, 18: 14118}, - ("Category Twos and Threes", 6, 8): {0: 2367, 9: 10679, 12: 18916, 15: 38806, 18: 29232}, - ("Category Twos and Threes", 7, 1): {0: 5802, 3: 28169, 6: 26411, 9: 31169, 11: 8449}, - ("Category Twos and Threes", 7, 2): {0: 4415, 6: 34992, 9: 31238, 12: 20373, 14: 8982}, - ("Category Twos and Threes", 7, 3): {0: 471, 2: 8571, 6: 10929, 9: 28058, 12: 28900, 14: 14953, 16: 8118}, - ("Category Twos and Threes", 7, 4): {0: 3487, 6: 12139, 9: 14001, 12: 30314, 15: 23096, 18: 16963}, - ("Category Twos and Threes", 7, 5): {0: 40, 2: 7460, 12: 36006, 15: 31388, 18: 25106}, - ("Category Twos and Threes", 7, 6): {0: 3554, 9: 11611, 12: 15116, 15: 32501, 18: 27524, 20: 9694}, - ("Category Twos and Threes", 7, 7): {0: 157, 6: 8396, 13: 19880, 15: 22333, 18: 39121, 21: 10113}, - ("Category Twos and Threes", 7, 8): {0: 31, 5: 4682, 12: 14446, 15: 20934, 18: 44127, 21: 15780}, - ("Category Twos and Threes", 8, 1): {0: 3799, 3: 22551, 6: 23754, 8: 29290, 10: 11990, 12: 8616}, - ("Category Twos and Threes", 8, 2): {0: 902, 4: 14360, 6: 13750, 9: 29893, 13: 30770, 15: 10325}, - ("Category Twos and Threes", 8, 3): {0: 2221, 4: 8122, 9: 23734, 12: 28527, 16: 28942, 18: 8454}, - ("Category Twos and Threes", 8, 4): {0: 140, 3: 8344, 12: 33635, 15: 28711, 18: 20093, 20: 9077}, - ("Category Twos and Threes", 8, 5): {0: 3601, 9: 10269, 12: 12458, 15: 28017, 18: 24815, 21: 20840}, - ("Category Twos and Threes", 8, 6): {0: 4104, 11: 10100, 15: 25259, 18: 30949, 21: 29588}, - ("Category Twos and Threes", 8, 7): {0: 3336, 12: 10227, 15: 14149, 18: 31155, 21: 29325, 23: 11808}, - ("Category Twos and Threes", 8, 8): {3: 7, 5: 7726, 16: 17997, 18: 21517, 21: 40641, 24: 12112}, - ("Category Sum of Odds", 1, 1): {0: 50084, 1: 16488, 3: 16584, 5: 16844}, - ("Category Sum of Odds", 1, 2): {0: 44489, 3: 27886, 5: 27625}, - ("Category Sum of Odds", 1, 3): {0: 27892, 3: 32299, 5: 39809}, - ("Category Sum of Odds", 1, 4): {0: 30917, 3: 19299, 5: 49784}, - ("Category Sum of Odds", 1, 5): {0: 25892, 3: 15941, 5: 58167}, - ("Category Sum of Odds", 1, 6): {0: 21678, 3: 13224, 5: 65098}, - ("Category Sum of Odds", 1, 7): {0: 17840, 3: 11191, 5: 70969}, - ("Category Sum of Odds", 1, 8): {0: 14690, 5: 85310}, - ("Category Sum of Odds", 2, 1): {0: 24611, 1: 19615, 3: 22234, 6: 25168, 8: 8372}, - ("Category Sum of Odds", 2, 2): {0: 11216, 3: 33181, 6: 32416, 8: 15414, 10: 7773}, - ("Category Sum of Odds", 2, 3): {0: 13730, 3: 17055, 5: 34933, 8: 18363, 10: 15919}, - ("Category Sum of Odds", 2, 4): {0: 9599, 3: 11842, 5: 34490, 8: 19129, 10: 24940}, - ("Category Sum of Odds", 2, 5): {0: 6652, 5: 40845, 8: 18712, 10: 33791}, - ("Category Sum of Odds", 2, 6): {0: 10404, 5: 20970, 8: 26124, 10: 42502}, - ("Category Sum of Odds", 2, 7): {0: 7262, 5: 26824, 8: 15860, 10: 50054}, - ("Category Sum of Odds", 2, 8): {0: 4950, 5: 23253, 8: 14179, 10: 57618}, - ("Category Sum of Odds", 3, 1): {0: 12467, 1: 16736, 4: 20970, 6: 29252, 8: 11660, 10: 8915}, - ("Category Sum of Odds", 3, 2): {0: 8635, 3: 15579, 6: 27649, 9: 30585, 13: 17552}, - ("Category Sum of Odds", 3, 3): {0: 5022, 6: 32067, 8: 21631, 11: 24032, 13: 17248}, - ("Category Sum of Odds", 3, 4): {0: 8260, 6: 17955, 8: 18530, 11: 28631, 13: 14216, 15: 12408}, - ("Category Sum of Odds", 3, 5): {0: 4685, 5: 13863, 8: 14915, 11: 30363, 13: 16370, 15: 19804}, - ("Category Sum of Odds", 3, 6): {0: 2766, 5: 10213, 8: 11372, 10: 30968, 13: 17133, 15: 27548}, - ("Category Sum of Odds", 3, 7): {0: 543, 3: 8448, 10: 28784, 13: 26258, 15: 35967}, - ("Category Sum of Odds", 3, 8): {0: 3760, 6: 8911, 11: 27672, 13: 16221, 15: 43436}, - ("Category Sum of Odds", 4, 1): {0: 18870, 5: 28873, 6: 18550, 9: 20881, 11: 12826}, - ("Category Sum of Odds", 4, 2): {0: 7974, 6: 23957, 9: 27982, 11: 15953, 13: 13643, 15: 10491}, - ("Category Sum of Odds", 4, 3): {0: 1778, 3: 8154, 8: 25036, 11: 24307, 13: 18030, 15: 14481, 18: 8214}, - ("Category Sum of Odds", 4, 4): {0: 1862, 4: 8889, 8: 11182, 11: 21997, 13: 19483, 16: 20879, 20: 15708}, - ("Category Sum of Odds", 4, 5): {0: 5687, 7: 8212, 11: 18674, 13: 17578, 16: 25572, 18: 12704, 20: 11573}, - ("Category Sum of Odds", 4, 6): {0: 6549, 11: 17161, 13: 15290, 16: 28355, 18: 14865, 20: 17780}, - ("Category Sum of Odds", 4, 7): {0: 5048, 10: 11824, 13: 12343, 16: 29544, 18: 15947, 20: 25294}, - ("Category Sum of Odds", 4, 8): {0: 3060, 10: 8747, 15: 29415, 18: 25762, 20: 33016}, - ("Category Sum of Odds", 5, 1): {0: 3061, 3: 22078, 6: 26935, 9: 23674, 11: 15144, 14: 9108}, - ("Category Sum of Odds", 5, 2): {0: 5813, 7: 19297, 9: 14666, 11: 17165, 14: 21681, 16: 10586, 18: 10792}, - ("Category Sum of Odds", 5, 3): {0: 3881, 6: 9272, 9: 10300, 11: 13443, 14: 24313, 16: 13969, 19: 16420, 21: 8402}, - ("Category Sum of Odds", 5, 4): {0: 4213, 8: 9656, 13: 24199, 16: 22188, 18: 16440, 20: 14313, 23: 8991}, - ("Category Sum of Odds", 5, 5): {0: 4997, 10: 9128, 13: 11376, 16: 20859, 18: 17548, 21: 20120, 25: 15972}, - ("Category Sum of Odds", 5, 6): { - 0: 4581, - 11: 8516, - 14: 11335, - 16: 10647, - 18: 16866, - 21: 24256, - 23: 11945, - 25: 11854, - }, - ("Category Sum of Odds", 5, 7): {0: 176, 6: 8052, 16: 17535, 18: 14878, 21: 27189, 23: 14100, 25: 18070}, - ("Category Sum of Odds", 5, 8): {0: 2, 2: 6622, 15: 12097, 18: 12454, 21: 28398, 23: 15254, 25: 25173}, - ("Category Sum of Odds", 6, 1): {0: 11634, 4: 12188, 6: 16257, 9: 23909, 11: 13671, 13: 13125, 16: 9216}, - ("Category Sum of Odds", 6, 2): {0: 1403, 4: 8241, 10: 22151, 12: 14245, 14: 15279, 17: 19690, 21: 18991}, - ("Category Sum of Odds", 6, 3): { - 0: 6079, - 9: 10832, - 12: 10094, - 14: 13221, - 17: 22538, - 19: 12673, - 21: 15363, - 24: 9200, - }, - ("Category Sum of Odds", 6, 4): {0: 5771, 11: 9419, 16: 22239, 19: 22715, 21: 12847, 23: 12798, 25: 9237, 28: 4974}, - ("Category Sum of Odds", 6, 5): { - 0: 2564, - 11: 8518, - 17: 20753, - 19: 14121, - 21: 13179, - 23: 15752, - 25: 14841, - 28: 10272, - }, - ("Category Sum of Odds", 6, 6): {0: 4310, 14: 8668, 19: 20891, 21: 12052, 23: 16882, 26: 19954, 30: 17243}, - ("Category Sum of Odds", 6, 7): { - 0: 5233, - 16: 8503, - 19: 11127, - 21: 10285, - 23: 16141, - 26: 23993, - 28: 12043, - 30: 12675, - }, - ("Category Sum of Odds", 6, 8): {0: 510, 12: 8107, 21: 17013, 23: 14396, 26: 26771, 28: 13964, 30: 19239}, - ("Category Sum of Odds", 7, 1): { - 0: 2591, - 2: 8436, - 5: 11759, - 7: 13733, - 9: 15656, - 11: 14851, - 13: 12301, - 15: 11871, - 18: 8802, - }, - ("Category Sum of Odds", 7, 2): { - 0: 4730, - 8: 8998, - 11: 10573, - 13: 13099, - 15: 13819, - 17: 13594, - 19: 12561, - 21: 12881, - 24: 9745, - }, - ("Category Sum of Odds", 7, 3): { - 0: 2549, - 9: 8523, - 15: 19566, - 17: 12251, - 19: 13562, - 21: 13473, - 23: 11918, - 27: 18158, - }, - ("Category Sum of Odds", 7, 4): {0: 6776, 14: 9986, 19: 20914, 22: 21006, 24: 12685, 26: 10835, 30: 17798}, - ("Category Sum of Odds", 7, 5): { - 0: 2943, - 14: 8009, - 20: 20248, - 22: 11896, - 24: 14166, - 26: 12505, - 28: 13136, - 30: 10486, - 33: 6611, - }, - ("Category Sum of Odds", 7, 6): { - 2: 1990, - 15: 8986, - 22: 19198, - 24: 13388, - 26: 12513, - 28: 15893, - 30: 15831, - 35: 12201, - }, - ("Category Sum of Odds", 7, 7): { - 4: 559, - 14: 8153, - 21: 11671, - 24: 12064, - 26: 11473, - 28: 16014, - 31: 20785, - 33: 10174, - 35: 9107, - }, - ("Category Sum of Odds", 7, 8): {0: 3, 8: 5190, 21: 8049, 24: 10585, 28: 25255, 31: 24333, 33: 12445, 35: 14140}, - ("Category Sum of Odds", 8, 1): {0: 7169, 7: 19762, 9: 14044, 11: 14858, 13: 13399, 15: 10801, 17: 11147, 20: 8820}, - ("Category Sum of Odds", 8, 2): { - 0: 7745, - 11: 10927, - 14: 10849, - 16: 13103, - 18: 13484, - 20: 12487, - 22: 10815, - 24: 11552, - 27: 9038, - }, - ("Category Sum of Odds", 8, 3): { - 0: 3867, - 12: 9356, - 18: 19408, - 20: 12379, - 22: 12519, - 24: 12260, - 26: 11008, - 28: 10726, - 31: 8477, - }, - ("Category Sum of Odds", 8, 4): { - 1: 3971, - 15: 9176, - 21: 18732, - 23: 12900, - 25: 13405, - 27: 11603, - 29: 10400, - 33: 19813, - }, - ("Category Sum of Odds", 8, 5): { - 1: 490, - 12: 8049, - 20: 9682, - 23: 10177, - 25: 12856, - 27: 12369, - 29: 12781, - 32: 18029, - 34: 11315, - 38: 4252, - }, - ("Category Sum of Odds", 8, 6): { - 4: 86, - 11: 8038, - 22: 9157, - 25: 10729, - 27: 11053, - 29: 13606, - 31: 12383, - 33: 14068, - 35: 12408, - 38: 8472, - }, - ("Category Sum of Odds", 8, 7): { - 6: 1852, - 20: 8020, - 27: 17455, - 29: 12898, - 31: 12181, - 33: 15650, - 35: 17577, - 40: 14367, - }, - ("Category Sum of Odds", 8, 8): { - 4: 8, - 11: 8008, - 26: 10314, - 29: 11446, - 31: 10714, - 33: 16060, - 36: 21765, - 38: 10622, - 40: 11063, - }, - ("Category Sum of Evens", 1, 1): {0: 49585, 2: 16733, 4: 16854, 6: 16828}, - ("Category Sum of Evens", 1, 2): {0: 33244, 2: 11087, 4: 28025, 6: 27644}, - ("Category Sum of Evens", 1, 3): {0: 22259, 4: 42357, 6: 35384}, - ("Category Sum of Evens", 1, 4): {0: 18511, 4: 35651, 6: 45838}, - ("Category Sum of Evens", 1, 5): {0: 15428, 4: 29656, 6: 54916}, - ("Category Sum of Evens", 1, 6): {0: 12927, 4: 24370, 6: 62703}, - ("Category Sum of Evens", 1, 7): {0: 14152, 4: 17087, 6: 68761}, - ("Category Sum of Evens", 1, 8): {0: 11920, 4: 14227, 6: 73853}, - ("Category Sum of Evens", 2, 1): {0: 25229, 2: 16545, 4: 19538, 6: 21987, 10: 16701}, - ("Category Sum of Evens", 2, 2): {0: 11179, 4: 27164, 6: 24451, 8: 13966, 10: 15400, 12: 7840}, - ("Category Sum of Evens", 2, 3): {0: 8099, 4: 16354, 6: 20647, 8: 17887, 10: 24736, 12: 12277}, - ("Category Sum of Evens", 2, 4): {0: 5687, 4: 11219, 6: 20711, 8: 14290, 10: 26976, 12: 21117}, - ("Category Sum of Evens", 2, 5): {0: 3991, 6: 27157, 8: 11641, 10: 26842, 12: 30369}, - ("Category Sum of Evens", 2, 6): {0: 2741, 6: 23123, 10: 35050, 12: 39086}, - ("Category Sum of Evens", 2, 7): {0: 1122, 6: 20538, 10: 30952, 12: 47388}, - ("Category Sum of Evens", 2, 8): {0: 3950, 6: 14006, 10: 27341, 12: 54703}, - ("Category Sum of Evens", 3, 1): {0: 12538, 2: 12516, 4: 16530, 6: 21270, 8: 13745, 10: 11209, 14: 12192}, - ("Category Sum of Evens", 3, 2): {0: 7404, 4: 10459, 6: 15644, 8: 15032, 10: 18955, 12: 15021, 16: 17485}, - ("Category Sum of Evens", 3, 3): {0: 2176, 6: 14148, 8: 12295, 10: 20247, 12: 18001, 14: 15953, 16: 17180}, - ("Category Sum of Evens", 3, 4): {0: 4556, 8: 15062, 10: 17232, 12: 18975, 14: 15832, 16: 18749, 18: 9594}, - ("Category Sum of Evens", 3, 5): {0: 2575, 8: 10825, 10: 13927, 12: 19533, 14: 14402, 16: 21954, 18: 16784}, - ("Category Sum of Evens", 3, 6): {0: 1475, 6: 7528, 10: 10614, 12: 19070, 14: 12940, 16: 23882, 18: 24491}, - ("Category Sum of Evens", 3, 7): {0: 862, 6: 5321, 12: 26291, 14: 10985, 16: 24254, 18: 32287}, - ("Category Sum of Evens", 3, 8): {0: 138, 4: 4086, 12: 22703, 16: 32516, 18: 40557}, - ("Category Sum of Evens", 4, 1): {0: 6214, 4: 20921, 6: 17434, 8: 15427, 10: 14158, 12: 11354, 16: 14492}, - ("Category Sum of Evens", 4, 2): { - 0: 2868, - 6: 13362, - 8: 10702, - 10: 15154, - 12: 15715, - 14: 14104, - 16: 12485, - 20: 15610, - }, - ("Category Sum of Evens", 4, 3): { - 0: 573, - 8: 10496, - 10: 10269, - 12: 12879, - 14: 16224, - 16: 17484, - 18: 13847, - 20: 10518, - 22: 7710, - }, - ("Category Sum of Evens", 4, 4): { - 0: 1119, - 6: 5124, - 12: 17394, - 14: 12763, - 16: 17947, - 18: 16566, - 20: 13338, - 22: 15749, - }, - ("Category Sum of Evens", 4, 5): {0: 3477, 12: 12738, 16: 26184, 18: 18045, 20: 14172, 22: 16111, 24: 9273}, - ("Category Sum of Evens", 4, 6): {0: 991, 12: 10136, 16: 21089, 18: 18805, 20: 13848, 22: 20013, 24: 15118}, - ("Category Sum of Evens", 4, 7): {0: 2931, 16: 21174, 18: 18952, 20: 12601, 22: 21947, 24: 22395}, - ("Category Sum of Evens", 4, 8): {0: 1798, 12: 6781, 18: 27146, 20: 11505, 22: 23056, 24: 29714}, - ("Category Sum of Evens", 5, 1): { - 0: 3192, - 4: 13829, - 6: 13373, - 8: 13964, - 10: 14656, - 12: 13468, - 14: 10245, - 18: 17273, - }, - ("Category Sum of Evens", 5, 2): { - 0: 3217, - 8: 10390, - 12: 22094, - 14: 13824, - 16: 14674, - 18: 12124, - 22: 16619, - 24: 7058, - }, - ("Category Sum of Evens", 5, 3): { - 0: 3904, - 12: 11004, - 14: 10339, - 16: 13128, - 18: 14686, - 20: 15282, - 22: 13294, - 26: 18363, - }, - ("Category Sum of Evens", 5, 4): { - 0: 43, - 4: 4025, - 14: 10648, - 16: 10437, - 18: 12724, - 20: 14710, - 22: 16005, - 24: 12896, - 28: 18512, - }, - ("Category Sum of Evens", 5, 5): { - 0: 350, - 8: 4392, - 16: 11641, - 18: 10297, - 20: 12344, - 22: 16826, - 24: 15490, - 26: 12235, - 28: 16425, - }, - ("Category Sum of Evens", 5, 6): { - 0: 374, - 10: 4670, - 18: 13498, - 22: 25729, - 24: 17286, - 26: 13565, - 28: 15274, - 30: 9604, - }, - ("Category Sum of Evens", 5, 7): {0: 1473, 18: 11310, 22: 21341, 24: 18114, 26: 13349, 28: 19048, 30: 15365}, - ("Category Sum of Evens", 5, 8): {0: 1, 4: 3753, 20: 10318, 22: 11699, 24: 18376, 26: 12500, 28: 21211, 30: 22142}, - ("Category Sum of Evens", 6, 1): { - 0: 4767, - 6: 15250, - 8: 11527, - 10: 13220, - 12: 13855, - 14: 12217, - 16: 10036, - 20: 19128, - }, - ("Category Sum of Evens", 6, 2): { - 0: 1380, - 6: 5285, - 12: 13888, - 14: 10495, - 16: 12112, - 18: 12962, - 20: 12458, - 22: 10842, - 26: 14076, - 28: 6502, - }, - ("Category Sum of Evens", 6, 3): { - 0: 1230, - 16: 17521, - 18: 10098, - 20: 12628, - 22: 13809, - 24: 13594, - 26: 11930, - 30: 19190, - }, - ("Category Sum of Evens", 6, 4): {0: 1235, 18: 15534, 22: 22081, 24: 13471, 26: 13991, 28: 12906, 32: 20782}, - ("Category Sum of Evens", 6, 5): {0: 1241, 20: 15114, 24: 21726, 26: 13874, 28: 15232, 30: 12927, 34: 19886}, - ("Category Sum of Evens", 6, 6): {0: 1224, 22: 15886, 26: 21708, 28: 15982, 30: 15534, 32: 12014, 34: 17652}, - ("Category Sum of Evens", 6, 7): {4: 1437, 24: 17624, 28: 24727, 30: 17083, 32: 13001, 34: 15604, 36: 10524}, - ("Category Sum of Evens", 6, 8): {4: 1707, 24: 11310, 28: 20871, 30: 18101, 32: 12842, 34: 18840, 36: 16329}, - ("Category Sum of Evens", 7, 1): { - 0: 6237, - 8: 15390, - 10: 11183, - 12: 12690, - 14: 12463, - 16: 11578, - 20: 17339, - 22: 8870, - 26: 4250, - }, - ("Category Sum of Evens", 7, 2): { - 0: 1433, - 14: 16705, - 18: 19797, - 20: 11747, - 22: 12101, - 24: 10947, - 28: 16547, - 32: 10723, - }, - ("Category Sum of Evens", 7, 3): { - 0: 2135, - 14: 5836, - 20: 13766, - 22: 10305, - 24: 12043, - 26: 13153, - 28: 12644, - 30: 10884, - 34: 19234, - }, - ("Category Sum of Evens", 7, 4): { - 0: 1762, - 22: 16471, - 26: 20839, - 28: 12907, - 30: 13018, - 32: 11907, - 34: 10022, - 38: 13074, - }, - ("Category Sum of Evens", 7, 5): { - 4: 1630, - 24: 14719, - 28: 20377, - 30: 12713, - 32: 13273, - 34: 13412, - 36: 10366, - 40: 13510, - }, - ("Category Sum of Evens", 7, 6): { - 4: 1436, - 26: 14275, - 30: 20680, - 32: 12798, - 34: 15385, - 36: 13346, - 38: 10011, - 40: 12069, - }, - ("Category Sum of Evens", 7, 7): { - 6: 2815, - 24: 6584, - 30: 16532, - 32: 11106, - 34: 15613, - 36: 15702, - 38: 12021, - 40: 12478, - 42: 7149, - }, - ("Category Sum of Evens", 7, 8): {10: 1490, 30: 16831, 34: 23888, 36: 16970, 38: 12599, 40: 16137, 42: 12085}, - ("Category Sum of Evens", 8, 1): { - 0: 3709, - 8: 10876, - 12: 19246, - 14: 11696, - 16: 11862, - 18: 11145, - 22: 16877, - 24: 9272, - 28: 5317, - }, - ("Category Sum of Evens", 8, 2): { - 0: 1361, - 16: 14530, - 20: 17637, - 22: 10922, - 24: 11148, - 26: 10879, - 30: 17754, - 34: 15769, - }, - ("Category Sum of Evens", 8, 3): { - 2: 1601, - 22: 14895, - 26: 18464, - 28: 11561, - 30: 12249, - 32: 11747, - 34: 10070, - 38: 19413, - }, - ("Category Sum of Evens", 8, 4): { - 0: 2339, - 20: 5286, - 26: 11746, - 30: 19858, - 32: 12344, - 34: 12243, - 36: 11307, - 40: 16632, - 42: 8245, - }, - ("Category Sum of Evens", 8, 5): { - 4: 1798, - 28: 14824, - 32: 18663, - 34: 12180, - 36: 12458, - 38: 12260, - 40: 10958, - 44: 16859, - }, - ("Category Sum of Evens", 8, 6): { - 6: 2908, - 26: 6292, - 32: 13573, - 34: 10367, - 36: 12064, - 38: 12862, - 40: 13920, - 42: 11359, - 46: 16655, - }, - ("Category Sum of Evens", 8, 7): { - 8: 2652, - 28: 6168, - 34: 13922, - 36: 10651, - 38: 12089, - 40: 14999, - 42: 13899, - 44: 10574, - 46: 15046, - }, - ("Category Sum of Evens", 8, 8): { - 10: 2547, - 30: 6023, - 36: 15354, - 38: 10354, - 40: 14996, - 42: 16214, - 44: 11803, - 46: 13670, - 48: 9039, - }, - ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 16591, 8: 16660}, - ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 27694, 8: 27631}, - ("Category Double Threes and Fours", 1, 3): {0: 29592, 6: 35261, 8: 35147}, - ("Category Double Threes and Fours", 1, 4): {0: 24601, 6: 29406, 8: 45993}, - ("Category Double Threes and Fours", 1, 5): {0: 20499, 6: 24420, 8: 55081}, - ("Category Double Threes and Fours", 1, 6): {0: 17116, 6: 20227, 8: 62657}, - ("Category Double Threes and Fours", 1, 7): {0: 14193, 6: 17060, 8: 68747}, - ("Category Double Threes and Fours", 1, 8): {0: 11977, 6: 13924, 8: 74099}, - ("Category Double Threes and Fours", 2, 1): {0: 44382, 6: 22191, 8: 22251, 14: 11176}, - ("Category Double Threes and Fours", 2, 2): {0: 19720, 6: 24652, 8: 24891, 14: 23096, 16: 7641}, - ("Category Double Threes and Fours", 2, 3): {0: 8765, 6: 21008, 8: 20929, 12: 12201, 14: 24721, 16: 12376}, - ("Category Double Threes and Fours", 2, 4): {0: 6164, 6: 14466, 8: 22828, 14: 35406, 16: 21136}, - ("Category Double Threes and Fours", 2, 5): {0: 4307, 6: 10005, 8: 22620, 14: 32879, 16: 30189}, - ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 28513, 14: 29530, 16: 39078}, - ("Category Double Threes and Fours", 2, 7): {0: 2042, 8: 24335, 14: 26250, 16: 47373}, - ("Category Double Threes and Fours", 2, 8): {0: 1385, 8: 23166, 14: 20907, 16: 54542}, - ("Category Double Threes and Fours", 3, 1): {0: 29378, 6: 22335, 8: 22138, 14: 16783, 16: 9366}, - ("Category Double Threes and Fours", 3, 2): { - 0: 8894, - 6: 16518, - 8: 16277, - 12: 10334, - 14: 20757, - 16: 12265, - 22: 14955, - }, - ("Category Double Threes and Fours", 3, 3): { - 0: 2643, - 8: 18522, - 12: 11066, - 14: 21922, - 16: 11045, - 20: 17235, - 22: 17567, - }, - ("Category Double Threes and Fours", 3, 4): { - 0: 1523, - 8: 13773, - 14: 26533, - 16: 18276, - 20: 11695, - 22: 18521, - 24: 9679, - }, - ("Category Double Threes and Fours", 3, 5): {0: 845, 8: 10218, 14: 20245, 16: 20293, 22: 31908, 24: 16491}, - ("Category Double Threes and Fours", 3, 6): {0: 499, 8: 7230, 14: 15028, 16: 20914, 22: 31835, 24: 24494}, - ("Category Double Threes and Fours", 3, 7): {0: 1298, 8: 5434, 16: 30595, 22: 29980, 24: 32693}, - ("Category Double Threes and Fours", 3, 8): {0: 178, 6: 4363, 16: 27419, 22: 27614, 24: 40426}, - ("Category Double Threes and Fours", 4, 1): {0: 19809, 6: 19538, 8: 19765, 14: 22348, 18: 12403, 22: 6137}, - ("Category Double Threes and Fours", 4, 2): { - 0: 3972, - 8: 19440, - 14: 27646, - 16: 12978, - 20: 11442, - 22: 11245, - 24: 6728, - 28: 6549, - }, - ("Category Double Threes and Fours", 4, 3): { - 0: 745, - 6: 7209, - 14: 19403, - 18: 11744, - 20: 15371, - 22: 15441, - 26: 13062, - 30: 17025, - }, - ("Category Double Threes and Fours", 4, 4): { - 0: 371, - 6: 4491, - 14: 13120, - 16: 10176, - 20: 11583, - 22: 18508, - 24: 10280, - 28: 15624, - 30: 15847, - }, - ("Category Double Threes and Fours", 4, 5): { - 0: 163, - 6: 4251, - 16: 15796, - 22: 26145, - 24: 17306, - 28: 10930, - 30: 16244, - 32: 9165, - }, - ("Category Double Threes and Fours", 4, 6): {0: 79, 16: 14439, 22: 21763, 24: 18861, 30: 29518, 32: 15340}, - ("Category Double Threes and Fours", 4, 7): {0: 1042, 16: 12543, 22: 13634, 24: 20162, 30: 30259, 32: 22360}, - ("Category Double Threes and Fours", 4, 8): {0: 20, 6: 2490, 16: 6901, 22: 10960, 24: 20269, 30: 29442, 32: 29918}, - ("Category Double Threes and Fours", 5, 1): { - 0: 13122, - 6: 16411, - 8: 16451, - 14: 24768, - 16: 10392, - 22: 14528, - 26: 4328, - }, - ("Category Double Threes and Fours", 5, 2): { - 0: 1676, - 8: 10787, - 14: 20218, - 18: 11102, - 20: 12668, - 22: 12832, - 26: 10994, - 30: 15390, - 34: 4333, - }, - ("Category Double Threes and Fours", 5, 3): { - 0: 223, - 14: 12365, - 16: 7165, - 20: 11385, - 22: 11613, - 26: 15182, - 28: 13665, - 32: 14400, - 36: 14002, - }, - ("Category Double Threes and Fours", 5, 4): { - 0: 95, - 6: 2712, - 16: 8862, - 22: 18696, - 26: 12373, - 28: 13488, - 30: 14319, - 34: 12414, - 38: 17041, - }, - ("Category Double Threes and Fours", 5, 5): { - 0: 1333, - 14: 5458, - 22: 13613, - 24: 10772, - 28: 11201, - 30: 16810, - 32: 10248, - 36: 14426, - 38: 16139, - }, - ("Category Double Threes and Fours", 5, 6): { - 0: 16, - 16: 6354, - 24: 16213, - 30: 25369, - 32: 16845, - 36: 10243, - 38: 15569, - 40: 9391, - }, - ("Category Double Threes and Fours", 5, 7): { - 0: 161, - 12: 3457, - 24: 12437, - 30: 21495, - 32: 18636, - 38: 28581, - 40: 15233, - }, - ("Category Double Threes and Fours", 5, 8): { - 0: 478, - 16: 4861, - 26: 10119, - 30: 13694, - 32: 19681, - 38: 29177, - 40: 21990, - }, - ("Category Double Threes and Fours", 6, 1): { - 0: 8738, - 6: 13463, - 8: 12988, - 14: 24653, - 16: 11068, - 22: 19621, - 26: 5157, - 30: 4312, - }, - ("Category Double Threes and Fours", 6, 2): { - 0: 784, - 6: 5735, - 14: 13407, - 16: 8170, - 20: 11349, - 22: 11356, - 26: 12465, - 28: 10790, - 30: 11527, - 38: 14417, - }, - ("Category Double Threes and Fours", 6, 3): { - 0: 72, - 14: 8986, - 22: 13700, - 26: 12357, - 28: 12114, - 32: 15882, - 36: 19286, - 40: 13540, - 44: 4063, - }, - ("Category Double Threes and Fours", 6, 4): { - 0: 439, - 18: 7427, - 22: 9284, - 28: 14203, - 30: 10836, - 34: 14646, - 36: 12511, - 38: 10194, - 42: 10202, - 46: 10258, - }, - ("Category Double Threes and Fours", 6, 5): { - 0: 166, - 20: 7618, - 24: 5198, - 30: 17479, - 34: 12496, - 36: 12190, - 38: 14163, - 42: 12571, - 46: 18119, - }, - ("Category Double Threes and Fours", 6, 6): { - 0: 1843, - 22: 5905, - 30: 12997, - 32: 10631, - 36: 10342, - 38: 16439, - 40: 10795, - 44: 13485, - 46: 17563, - }, - ("Category Double Threes and Fours", 6, 7): { - 0: 31, - 12: 2221, - 24: 5004, - 32: 15743, - 38: 24402, - 40: 17005, - 46: 25241, - 48: 10353, - }, - ("Category Double Threes and Fours", 6, 8): { - 8: 79, - 16: 4037, - 32: 12559, - 38: 20863, - 40: 18347, - 46: 27683, - 48: 16432, - }, - ("Category Double Threes and Fours", 7, 1): { - 0: 5803, - 6: 10242, - 8: 10404, - 14: 22886, - 16: 10934, - 22: 19133, - 24: 7193, - 28: 8167, - 32: 5238, - }, - ("Category Double Threes and Fours", 7, 2): { - 0: 357, - 14: 17082, - 22: 17524, - 26: 11974, - 28: 11132, - 32: 13186, - 36: 13959, - 40: 10028, - 44: 4758, - }, - ("Category Double Threes and Fours", 7, 3): { - 0: 361, - 18: 7136, - 22: 5983, - 28: 13899, - 32: 12974, - 34: 10088, - 36: 10081, - 40: 14481, - 44: 14127, - 46: 6547, - 50: 4323, - }, - ("Category Double Threes and Fours", 7, 4): { - 0: 1182, - 18: 4299, - 30: 16331, - 34: 11316, - 36: 10741, - 40: 16028, - 44: 18815, - 48: 15225, - 52: 6063, - }, - ("Category Double Threes and Fours", 7, 5): { - 0: 45, - 12: 3763, - 32: 17140, - 38: 19112, - 42: 13655, - 44: 11990, - 46: 11137, - 50: 10646, - 54: 12512, - }, - ("Category Double Threes and Fours", 7, 6): { - 8: 2400, - 28: 5277, - 32: 5084, - 38: 16047, - 42: 12133, - 44: 11451, - 46: 14027, - 50: 13198, - 54: 20383, - }, - ("Category Double Threes and Fours", 7, 7): { - 6: 1968, - 30: 5585, - 38: 12210, - 40: 10376, - 46: 25548, - 48: 15392, - 54: 21666, - 56: 7255, - }, - ("Category Double Threes and Fours", 7, 8): { - 8: 42, - 20: 2293, - 32: 4653, - 40: 15068, - 46: 23170, - 48: 17057, - 54: 25601, - 56: 12116, - }, - ("Category Double Threes and Fours", 8, 1): { - 0: 3982, - 8: 15658, - 14: 20388, - 16: 10234, - 20: 10167, - 22: 10162, - 28: 15330, - 32: 8758, - 36: 5321, - }, - ("Category Double Threes and Fours", 8, 2): { - 0: 161, - 6: 3169, - 14: 7106, - 22: 16559, - 28: 16400, - 32: 12950, - 36: 16399, - 40: 10090, - 44: 11474, - 48: 5692, - }, - ("Category Double Threes and Fours", 8, 3): { - 0: 856, - 16: 4092, - 30: 13686, - 34: 12838, - 38: 15010, - 42: 17085, - 46: 14067, - 50: 11844, - 52: 6500, - 56: 4022, - }, - ("Category Double Threes and Fours", 8, 4): { - 0: 36, - 12: 2795, - 30: 9742, - 36: 11726, - 40: 12404, - 44: 18791, - 48: 14662, - 52: 15518, - 54: 8066, - 58: 6260, - }, - ("Category Double Threes and Fours", 8, 5): { - 6: 8, - 12: 2948, - 30: 5791, - 38: 10658, - 42: 10175, - 46: 19359, - 50: 14449, - 52: 10531, - 56: 13257, - 60: 12824, - }, - ("Category Double Threes and Fours", 8, 6): { - 0: 2, - 12: 2528, - 32: 4832, - 40: 11436, - 46: 17832, - 50: 13016, - 52: 11631, - 54: 12058, - 58: 11458, - 62: 15207, - }, - ("Category Double Threes and Fours", 8, 7): { - 6: 2, - 12: 2204, - 40: 9320, - 46: 14688, - 50: 11494, - 52: 10602, - 54: 14541, - 58: 13849, - 62: 23300, - }, - ("Category Double Threes and Fours", 8, 8): { - 8: 1, - 16: 1773, - 42: 8766, - 48: 17452, - 54: 24338, - 56: 15722, - 62: 22745, - 64: 9203, - }, - ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 4: 16803, 8: 16630}, - ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 4: 27448, 8: 27743}, - ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 4: 23184, 8: 39716}, - ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 4: 19221, 8: 49816}, - ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 4: 16079, 8: 58605}, - ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 4: 13237, 8: 65258}, - ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 4: 11100, 8: 71224}, - ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 4: 9323, 8: 75706}, - ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 24842, 12: 8319}, - ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 15172, 16: 7713}, - ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 4: 17158, 8: 34907, 12: 18539, 16: 15630}, - ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 4: 11981, 8: 34465, 12: 19108, 16: 24903}, - ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 4: 8302, 8: 32470, 12: 18612, 16: 34144}, - ("Category Quadruple Ones and Twos", 2, 6): {0: 4569, 4: 5737, 8: 29716, 12: 17216, 16: 42762}, - ("Category Quadruple Ones and Twos", 2, 7): {0: 3146, 8: 30463, 12: 15756, 16: 50635}, - ("Category Quadruple Ones and Twos", 2, 8): {0: 2265, 8: 26302, 12: 14167, 16: 57266}, - ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 11557, 16: 8682}, - ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 16799, 20: 8629}, - ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 4: 9447, 8: 22255, 12: 21685, 16: 24084, 20: 11167, 24: 6299}, - ("Category Quadruple Ones and Twos", 3, 4): { - 0: 2864, - 4: 5531, - 8: 17681, - 12: 18400, - 16: 28524, - 20: 14552, - 24: 12448, - }, - ("Category Quadruple Ones and Twos", 3, 5): {0: 1676, 8: 16697, 12: 14755, 16: 30427, 20: 16602, 24: 19843}, - ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 8: 10259, 12: 11326, 16: 31125, 20: 16984, 24: 27625}, - ("Category Quadruple Ones and Twos", 3, 7): {0: 1688, 8: 7543, 12: 8769, 16: 29367, 20: 17085, 24: 35548}, - ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 8: 5277, 12: 6388, 16: 27741, 20: 16170, 24: 43483}, - ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 11167, 24: 6071}, - ("Category Quadruple Ones and Twos", 4, 2): { - 0: 4023, - 4: 9776, - 8: 19015, - 12: 22094, - 16: 20986, - 20: 13805, - 24: 10301, - }, - ("Category Quadruple Ones and Twos", 4, 3): { - 0: 1848, - 8: 17116, - 12: 16853, - 16: 22831, - 20: 18400, - 24: 14480, - 28: 8472, - }, - ("Category Quadruple Ones and Twos", 4, 4): { - 0: 930, - 8: 10375, - 12: 12063, - 16: 21220, - 20: 19266, - 24: 20615, - 28: 9443, - 32: 6088, - }, - ("Category Quadruple Ones and Twos", 4, 5): { - 0: 1561, - 12: 12612, - 16: 18209, - 20: 17910, - 24: 25474, - 28: 12864, - 32: 11370, - }, - ("Category Quadruple Ones and Twos", 4, 6): { - 0: 722, - 12: 7979, - 16: 14796, - 20: 15416, - 24: 28256, - 28: 14675, - 32: 18156, - }, - ("Category Quadruple Ones and Twos", 4, 7): { - 0: 115, - 12: 5304, - 16: 11547, - 20: 12289, - 24: 29181, - 28: 16052, - 32: 25512, - }, - ("Category Quadruple Ones and Twos", 4, 8): {0: 164, 8: 2971, 16: 8888, 20: 9679, 24: 28785, 28: 16180, 32: 33333}, - ("Category Quadruple Ones and Twos", 5, 1): { - 0: 13112, - 4: 16534, - 8: 24718, - 12: 18558, - 16: 14547, - 20: 7055, - 24: 5476, - }, - ("Category Quadruple Ones and Twos", 5, 2): { - 0: 1764, - 4: 5529, - 8: 12216, - 12: 17687, - 16: 20808, - 20: 18149, - 24: 12849, - 28: 6991, - 32: 4007, - }, - ("Category Quadruple Ones and Twos", 5, 3): { - 0: 719, - 8: 8523, - 12: 11074, - 16: 17322, - 20: 19002, - 24: 18643, - 28: 12827, - 32: 7960, - 36: 3930, - }, - ("Category Quadruple Ones and Twos", 5, 4): { - 0: 1152, - 12: 9790, - 16: 12913, - 20: 15867, - 24: 20749, - 28: 16398, - 32: 14218, - 36: 8913, - }, - ("Category Quadruple Ones and Twos", 5, 5): { - 0: 98, - 12: 5549, - 16: 8863, - 20: 12037, - 24: 20010, - 28: 17568, - 32: 19789, - 36: 9319, - 40: 6767, - }, - ("Category Quadruple Ones and Twos", 5, 6): { - 0: 194, - 8: 2663, - 16: 5734, - 20: 8436, - 24: 17830, - 28: 16864, - 32: 24246, - 36: 12115, - 40: 11918, - }, - ("Category Quadruple Ones and Twos", 5, 7): { - 0: 1449, - 20: 9396, - 24: 14936, - 28: 14969, - 32: 27238, - 36: 14094, - 40: 17918, - }, - ("Category Quadruple Ones and Twos", 5, 8): { - 0: 747, - 20: 6034, - 24: 11929, - 28: 12517, - 32: 28388, - 36: 15339, - 40: 25046, - }, - ("Category Quadruple Ones and Twos", 6, 1): { - 0: 8646, - 4: 13011, - 8: 21357, - 12: 19385, - 16: 17008, - 20: 10409, - 24: 6249, - 28: 3935, - }, - ("Category Quadruple Ones and Twos", 6, 2): { - 0: 844, - 8: 10311, - 12: 12792, - 16: 17480, - 20: 18814, - 24: 16492, - 28: 11889, - 32: 6893, - 36: 4485, - }, - ("Category Quadruple Ones and Twos", 6, 3): { - 0: 1241, - 12: 9634, - 16: 11685, - 20: 15584, - 24: 17967, - 28: 16506, - 32: 13314, - 36: 8034, - 40: 6035, - }, - ("Category Quadruple Ones and Twos", 6, 4): { - 0: 1745, - 16: 9804, - 20: 10562, - 24: 15746, - 28: 17174, - 32: 17787, - 36: 12820, - 40: 9289, - 44: 5073, - }, - ("Category Quadruple Ones and Twos", 6, 5): { - 0: 2076, - 20: 10247, - 24: 12264, - 28: 14810, - 32: 19588, - 36: 16002, - 40: 14682, - 44: 6410, - 48: 3921, - }, - ("Category Quadruple Ones and Twos", 6, 6): { - 0: 884, - 20: 5943, - 24: 8774, - 28: 11481, - 32: 19145, - 36: 16864, - 40: 19906, - 44: 9386, - 48: 7617, - }, - ("Category Quadruple Ones and Twos", 6, 7): { - 0: 1386, - 24: 8138, - 28: 8372, - 32: 17207, - 36: 16148, - 40: 24051, - 44: 11862, - 48: 12836, - }, - ("Category Quadruple Ones and Twos", 6, 8): { - 0: 1841, - 28: 9606, - 32: 14489, - 36: 14585, - 40: 26779, - 44: 13821, - 48: 18879, - }, - ("Category Quadruple Ones and Twos", 7, 1): { - 0: 5780, - 4: 10185, - 8: 17905, - 12: 18364, - 16: 18160, - 20: 13115, - 24: 8617, - 32: 7874, - }, - ("Category Quadruple Ones and Twos", 7, 2): { - 0: 1795, - 12: 12828, - 16: 13204, - 20: 16895, - 24: 17562, - 28: 15061, - 32: 11122, - 36: 6507, - 40: 5026, - }, - ("Category Quadruple Ones and Twos", 7, 3): { - 0: 2065, - 16: 10495, - 20: 11008, - 24: 14839, - 28: 16393, - 32: 16118, - 36: 12681, - 40: 8773, - 48: 7628, - }, - ("Category Quadruple Ones and Twos", 7, 4): { - 0: 1950, - 20: 9612, - 24: 10535, - 28: 13596, - 32: 16527, - 36: 15938, - 40: 14071, - 44: 9192, - 48: 8579, - }, - ("Category Quadruple Ones and Twos", 7, 5): { - 0: 223, - 20: 5144, - 24: 6337, - 28: 9400, - 32: 14443, - 36: 15955, - 40: 17820, - 44: 13369, - 48: 10702, - 56: 6607, - }, - ("Category Quadruple Ones and Twos", 7, 6): { - 0: 271, - 24: 5976, - 28: 5988, - 32: 11398, - 36: 13738, - 40: 19063, - 44: 15587, - 48: 15867, - 52: 7202, - 56: 4910, - }, - ("Category Quadruple Ones and Twos", 7, 7): { - 0: 1032, - 28: 5724, - 32: 8275, - 36: 10801, - 40: 18184, - 44: 16470, - 48: 20467, - 52: 9969, - 56: 9078, - }, - ("Category Quadruple Ones and Twos", 7, 8): { - 0: 1508, - 32: 7832, - 36: 7770, - 40: 16197, - 44: 15477, - 48: 24388, - 52: 12403, - 56: 14425, - }, - ("Category Quadruple Ones and Twos", 8, 1): { - 0: 3811, - 4: 7682, - 8: 14638, - 12: 17214, - 16: 18191, - 20: 14651, - 24: 10976, - 28: 6591, - 36: 6246, - }, - ("Category Quadruple Ones and Twos", 8, 2): { - 0: 906, - 12: 7768, - 16: 9421, - 20: 13623, - 24: 16213, - 28: 16246, - 32: 14131, - 36: 10076, - 40: 6198, - 48: 5418, - }, - ("Category Quadruple Ones and Twos", 8, 3): { - 0: 224, - 8: 2520, - 20: 11222, - 24: 10733, - 28: 13934, - 32: 15751, - 36: 14882, - 40: 12409, - 44: 8920, - 48: 5462, - 52: 3943, - }, - ("Category Quadruple Ones and Twos", 8, 4): { - 0: 233, - 20: 5163, - 24: 6057, - 28: 9073, - 32: 12990, - 36: 14756, - 40: 15851, - 44: 13795, - 48: 10706, - 52: 6310, - 56: 5066, - }, - ("Category Quadruple Ones and Twos", 8, 5): { - 0: 76, - 12: 2105, - 28: 8316, - 32: 8993, - 36: 12039, - 40: 15561, - 44: 15382, - 48: 15278, - 52: 10629, - 56: 7377, - 60: 4244, - }, - ("Category Quadruple Ones and Twos", 8, 6): { - 4: 262, - 32: 10321, - 36: 8463, - 40: 13177, - 44: 14818, - 48: 17731, - 52: 14024, - 56: 12425, - 60: 5446, - 64: 3333, - }, - ("Category Quadruple Ones and Twos", 8, 7): { - 8: 300, - 32: 5443, - 36: 5454, - 40: 10276, - 44: 12582, - 48: 18487, - 52: 15549, - 56: 17187, - 60: 8149, - 64: 6573, - }, - ("Category Quadruple Ones and Twos", 8, 8): { - 8: 354, - 36: 5678, - 40: 7484, - 44: 9727, - 48: 17080, - 52: 15898, - 56: 21877, - 60: 10773, - 64: 11129, - }, + ("Category Half of Sixes", 8, 1): {0: 23337, 6: 76663}, + ("Category Half of Sixes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Half of Sixes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Half of Sixes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Half of Sixes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Half of Sixes", 8, 6): {0: 8804, 18: 91196}, + ("Category Half of Sixes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Half of Sixes", 8, 8): {0: 9252, 21: 90748}, + ("Category Twos and Threes", 1, 1): {0: 66466, 2: 33534}, + ("Category Twos and Threes", 1, 2): {0: 55640, 2: 44360}, + ("Category Twos and Threes", 1, 3): {0: 57822, 3: 42178}, + ("Category Twos and Threes", 1, 4): {0: 48170, 3: 51830}, + ("Category Twos and Threes", 1, 5): {0: 40294, 3: 59706}, + ("Category Twos and Threes", 1, 6): {0: 33417, 3: 66583}, + ("Category Twos and Threes", 1, 7): {0: 27852, 3: 72148}, + ("Category Twos and Threes", 1, 8): {0: 23364, 3: 76636}, + ("Category Twos and Threes", 2, 1): {0: 44565, 3: 55435}, + ("Category Twos and Threes", 2, 2): {0: 46335, 3: 53665}, + ("Category Twos and Threes", 2, 3): {0: 32347, 3: 67653}, + ("Category Twos and Threes", 2, 4): {0: 22424, 5: 77576}, + ("Category Twos and Threes", 2, 5): {0: 15661, 6: 84339}, + ("Category Twos and Threes", 2, 6): {0: 10775, 6: 89225}, + ("Category Twos and Threes", 2, 7): {0: 7375, 6: 92625}, + ("Category Twos and Threes", 2, 8): {0: 5212, 6: 94788}, + ("Category Twos and Threes", 3, 1): {0: 29892, 3: 70108}, + ("Category Twos and Threes", 3, 2): {0: 17285, 5: 82715}, + ("Category Twos and Threes", 3, 3): {0: 17436, 6: 82564}, + ("Category Twos and Threes", 3, 4): {0: 9962, 6: 90038}, + ("Category Twos and Threes", 3, 5): {0: 3347, 6: 96653}, + ("Category Twos and Threes", 3, 6): {0: 1821, 8: 98179}, + ("Category Twos and Threes", 3, 7): {0: 1082, 6: 61417, 9: 37501}, + ("Category Twos and Threes", 3, 8): {0: 13346, 9: 86654}, + ("Category Twos and Threes", 4, 1): {0: 19619, 5: 80381}, + ("Category Twos and Threes", 4, 2): {0: 18914, 6: 81086}, + ("Category Twos and Threes", 4, 3): {0: 4538, 5: 61859, 8: 33603}, + ("Category Twos and Threes", 4, 4): {0: 2183, 6: 62279, 9: 35538}, + ("Category Twos and Threes", 4, 5): {0: 16416, 9: 83584}, + ("Category Twos and Threes", 4, 6): {0: 6285, 9: 93715}, + ("Category Twos and Threes", 4, 7): {0: 30331, 11: 69669}, + ("Category Twos and Threes", 4, 8): {0: 22305, 12: 77695}, + ("Category Twos and Threes", 5, 1): {0: 13070, 5: 86930}, + ("Category Twos and Threes", 5, 2): {0: 5213, 5: 61441, 8: 33346}, + ("Category Twos and Threes", 5, 3): {0: 2126, 6: 58142, 9: 39732}, + ("Category Twos and Threes", 5, 4): {0: 848, 2: 30734, 11: 68418}, + ("Category Twos and Threes", 5, 5): {0: 29502, 12: 70498}, + ("Category Twos and Threes", 5, 6): {0: 123, 9: 52792, 12: 47085}, + ("Category Twos and Threes", 5, 7): {0: 8241, 12: 91759}, + ("Category Twos and Threes", 5, 8): {0: 13, 2: 31670, 14: 68317}, + ("Category Twos and Threes", 6, 1): {0: 22090, 6: 77910}, + ("Category Twos and Threes", 6, 2): {0: 2944, 6: 62394, 9: 34662}, + ("Category Twos and Threes", 6, 3): {0: 977, 2: 30626, 11: 68397}, + ("Category Twos and Threes", 6, 4): {0: 320, 8: 58370, 12: 41310}, + ("Category Twos and Threes", 6, 5): {0: 114, 2: 31718, 14: 68168}, + ("Category Twos and Threes", 6, 6): {0: 29669, 15: 70331}, + ("Category Twos and Threes", 6, 7): {0: 19855, 15: 80145}, + ("Category Twos and Threes", 6, 8): {0: 8524, 15: 91476}, + ("Category Twos and Threes", 7, 1): {0: 5802, 4: 54580, 7: 39618}, + ("Category Twos and Threes", 7, 2): {0: 1605, 6: 62574, 10: 35821}, + ("Category Twos and Threes", 7, 3): {0: 471, 8: 59691, 12: 39838}, + ("Category Twos and Threes", 7, 4): {0: 26620, 14: 73380}, + ("Category Twos and Threes", 7, 5): {0: 17308, 11: 37515, 15: 45177}, + ("Category Twos and Threes", 7, 6): {0: 30281, 17: 69719}, + ("Category Twos and Threes", 7, 7): {0: 28433, 18: 71567}, + ("Category Twos and Threes", 7, 8): {0: 13274, 18: 86726}, + ("Category Twos and Threes", 8, 1): {0: 3799, 5: 56614, 8: 39587}, + ("Category Twos and Threes", 8, 2): {0: 902, 7: 58003, 11: 41095}, + ("Category Twos and Threes", 8, 3): {0: 29391, 14: 70609}, + ("Category Twos and Threes", 8, 4): {0: 26041, 12: 40535, 16: 33424}, + ("Category Twos and Threes", 8, 5): {0: 26328, 14: 38760, 18: 34912}, + ("Category Twos and Threes", 8, 6): {0: 22646, 15: 45218, 19: 32136}, + ("Category Twos and Threes", 8, 7): {0: 25908, 20: 74092}, + ("Category Twos and Threes", 8, 8): {3: 18441, 17: 38826, 21: 42733}, + ("Category Sum of Odds", 1, 1): {0: 66572, 5: 33428}, + ("Category Sum of Odds", 1, 2): {0: 44489, 5: 55511}, + ("Category Sum of Odds", 1, 3): {0: 37185, 5: 62815}, + ("Category Sum of Odds", 1, 4): {0: 30917, 5: 69083}, + ("Category Sum of Odds", 1, 5): {0: 41833, 5: 58167}, + ("Category Sum of Odds", 1, 6): {0: 34902, 5: 65098}, + ("Category Sum of Odds", 1, 7): {0: 29031, 5: 70969}, + ("Category Sum of Odds", 1, 8): {0: 24051, 5: 75949}, + ("Category Sum of Odds", 2, 1): {0: 66460, 5: 33540}, + ("Category Sum of Odds", 2, 2): {0: 11216, 5: 65597, 8: 23187}, + ("Category Sum of Odds", 2, 3): {0: 30785, 8: 69215}, + ("Category Sum of Odds", 2, 4): {0: 21441, 10: 78559}, + ("Category Sum of Odds", 2, 5): {0: 14948, 10: 85052}, + ("Category Sum of Odds", 2, 6): {0: 4657, 3: 35569, 10: 59774}, + ("Category Sum of Odds", 2, 7): {0: 7262, 5: 42684, 10: 50054}, + ("Category Sum of Odds", 2, 8): {0: 4950, 5: 37432, 10: 57618}, + ("Category Sum of Odds", 3, 1): {0: 29203, 6: 70797}, + ("Category Sum of Odds", 3, 2): {0: 34454, 9: 65546}, + ("Category Sum of Odds", 3, 3): {0: 5022, 3: 32067, 8: 45663, 13: 17248}, + ("Category Sum of Odds", 3, 4): {0: 6138, 4: 33396, 13: 60466}, + ("Category Sum of Odds", 3, 5): {0: 29405, 15: 70595}, + ("Category Sum of Odds", 3, 6): {0: 21390, 15: 78610}, + ("Category Sum of Odds", 3, 7): {0: 8991, 8: 38279, 15: 52730}, + ("Category Sum of Odds", 3, 8): {0: 6340, 8: 34003, 15: 59657}, + ("Category Sum of Odds", 4, 1): {0: 28095, 4: 38198, 8: 33707}, + ("Category Sum of Odds", 4, 2): {0: 27003, 11: 72997}, + ("Category Sum of Odds", 4, 3): {0: 18712, 8: 40563, 13: 40725}, + ("Category Sum of Odds", 4, 4): {0: 30691, 15: 69309}, + ("Category Sum of Odds", 4, 5): {0: 433, 3: 32140, 13: 43150, 18: 24277}, + ("Category Sum of Odds", 4, 6): {0: 6549, 9: 32451, 15: 43220, 20: 17780}, + ("Category Sum of Odds", 4, 7): {0: 29215, 15: 45491, 20: 25294}, + ("Category Sum of Odds", 4, 8): {0: 11807, 13: 38927, 20: 49266}, + ("Category Sum of Odds", 5, 1): {0: 25139, 9: 74861}, + ("Category Sum of Odds", 5, 2): {0: 25110, 9: 40175, 14: 34715}, + ("Category Sum of Odds", 5, 3): {0: 23453, 11: 37756, 16: 38791}, + ("Category Sum of Odds", 5, 4): {0: 22993, 13: 37263, 18: 39744}, + ("Category Sum of Odds", 5, 5): {0: 25501, 15: 38407, 20: 36092}, + ("Category Sum of Odds", 5, 6): {0: 2542, 10: 32537, 18: 41122, 23: 23799}, + ("Category Sum of Odds", 5, 7): {0: 8228, 14: 32413, 20: 41289, 25: 18070}, + ("Category Sum of Odds", 5, 8): {0: 2, 2: 31173, 20: 43652, 25: 25173}, + ("Category Sum of Odds", 6, 1): {0: 23822, 6: 40166, 11: 36012}, + ("Category Sum of Odds", 6, 2): {0: 24182, 11: 37137, 16: 38681}, + ("Category Sum of Odds", 6, 3): {0: 27005, 14: 35759, 19: 37236}, + ("Category Sum of Odds", 6, 4): {0: 25133, 16: 35011, 21: 39856}, + ("Category Sum of Odds", 6, 5): {0: 24201, 18: 34934, 23: 40865}, + ("Category Sum of Odds", 6, 6): {0: 12978, 17: 32943, 23: 36836, 28: 17243}, + ("Category Sum of Odds", 6, 7): {0: 2314, 14: 32834, 23: 40134, 28: 24718}, + ("Category Sum of Odds", 6, 8): {0: 5464, 18: 34562, 25: 40735, 30: 19239}, + ("Category Sum of Odds", 7, 1): {0: 29329, 8: 37697, 13: 32974}, + ("Category Sum of Odds", 7, 2): {0: 29935, 14: 34878, 19: 35187}, + ("Category Sum of Odds", 7, 3): {0: 30638, 17: 33733, 22: 35629}, + ("Category Sum of Odds", 7, 4): {0: 163, 6: 32024, 20: 33870, 25: 33943}, + ("Category Sum of Odds", 7, 5): {0: 31200, 22: 35565, 27: 33235}, + ("Category Sum of Odds", 7, 6): {2: 30174, 24: 36670, 29: 33156}, + ("Category Sum of Odds", 7, 7): {4: 8712, 21: 35208, 28: 36799, 33: 19281}, + ("Category Sum of Odds", 7, 8): {0: 1447, 18: 32027, 28: 39941, 33: 26585}, + ("Category Sum of Odds", 8, 1): {0: 26931, 9: 35423, 14: 37646}, + ("Category Sum of Odds", 8, 2): {0: 29521, 16: 32919, 21: 37560}, + ("Category Sum of Odds", 8, 3): {0: 412, 7: 32219, 20: 32055, 25: 35314}, + ("Category Sum of Odds", 8, 4): {1: 27021, 22: 36376, 28: 36603}, + ("Category Sum of Odds", 8, 5): {1: 1069, 14: 32451, 26: 32884, 31: 33596}, + ("Category Sum of Odds", 8, 6): {4: 31598, 28: 33454, 33: 34948}, + ("Category Sum of Odds", 8, 7): {6: 27327, 29: 35647, 34: 37026}, + ("Category Sum of Odds", 8, 8): {4: 1, 26: 40489, 33: 37825, 38: 21685}, + ("Category Sum of Evens", 1, 1): {0: 49585, 6: 50415}, + ("Category Sum of Evens", 1, 2): {0: 44331, 6: 55669}, + ("Category Sum of Evens", 1, 3): {0: 29576, 6: 70424}, + ("Category Sum of Evens", 1, 4): {0: 24744, 6: 75256}, + ("Category Sum of Evens", 1, 5): {0: 20574, 6: 79426}, + ("Category Sum of Evens", 1, 6): {0: 17182, 6: 82818}, + ("Category Sum of Evens", 1, 7): {0: 14152, 6: 85848}, + ("Category Sum of Evens", 1, 8): {0: 8911, 6: 91089}, + ("Category Sum of Evens", 2, 1): {0: 25229, 8: 74771}, + ("Category Sum of Evens", 2, 2): {0: 18682, 6: 58078, 10: 23240}, + ("Category Sum of Evens", 2, 3): {0: 8099, 10: 91901}, + ("Category Sum of Evens", 2, 4): {0: 16906, 12: 83094}, + ("Category Sum of Evens", 2, 5): {0: 11901, 12: 88099}, + ("Category Sum of Evens", 2, 6): {0: 8054, 12: 91946}, + ("Category Sum of Evens", 2, 7): {0: 5695, 12: 94305}, + ("Category Sum of Evens", 2, 8): {0: 3950, 12: 96050}, + ("Category Sum of Evens", 3, 1): {0: 25054, 6: 51545, 10: 23401}, + ("Category Sum of Evens", 3, 2): {0: 17863, 10: 64652, 14: 17485}, + ("Category Sum of Evens", 3, 3): {0: 7748, 12: 75072, 16: 17180}, + ("Category Sum of Evens", 3, 4): {0: 1318, 12: 70339, 16: 28343}, + ("Category Sum of Evens", 3, 5): {0: 7680, 12: 53582, 18: 38738}, + ("Category Sum of Evens", 3, 6): {0: 1475, 12: 50152, 18: 48373}, + ("Category Sum of Evens", 3, 7): {0: 14328, 18: 85672}, + ("Category Sum of Evens", 3, 8): {0: 10001, 18: 89999}, + ("Category Sum of Evens", 4, 1): {0: 6214, 8: 67940, 12: 25846}, + ("Category Sum of Evens", 4, 2): {0: 16230, 12: 55675, 16: 28095}, + ("Category Sum of Evens", 4, 3): {0: 11069, 16: 70703, 20: 18228}, + ("Category Sum of Evens", 4, 4): {0: 13339, 20: 86661}, + ("Category Sum of Evens", 4, 5): {0: 8193, 18: 66423, 22: 25384}, + ("Category Sum of Evens", 4, 6): {0: 11127, 18: 53742, 22: 35131}, + ("Category Sum of Evens", 4, 7): {0: 7585, 18: 48073, 24: 44342}, + ("Category Sum of Evens", 4, 8): {0: 642, 18: 46588, 24: 52770}, + ("Category Sum of Evens", 5, 1): {0: 8373, 8: 50641, 16: 40986}, + ("Category Sum of Evens", 5, 2): {0: 7271, 12: 42254, 20: 50475}, + ("Category Sum of Evens", 5, 3): {0: 8350, 16: 44711, 24: 46939}, + ("Category Sum of Evens", 5, 4): {0: 8161, 18: 44426, 26: 47413}, + ("Category Sum of Evens", 5, 5): {0: 350, 8: 16033, 24: 67192, 28: 16425}, + ("Category Sum of Evens", 5, 6): {0: 10318, 24: 64804, 28: 24878}, + ("Category Sum of Evens", 5, 7): {0: 12783, 24: 52804, 28: 34413}, + ("Category Sum of Evens", 5, 8): {0: 1, 24: 56646, 30: 43353}, + ("Category Sum of Evens", 6, 1): {0: 10482, 10: 48137, 18: 41381}, + ("Category Sum of Evens", 6, 2): {0: 12446, 16: 43676, 24: 43878}, + ("Category Sum of Evens", 6, 3): {0: 11037, 20: 44249, 28: 44714}, + ("Category Sum of Evens", 6, 4): {0: 10005, 22: 42316, 30: 47679}, + ("Category Sum of Evens", 6, 5): {0: 9751, 24: 42204, 32: 48045}, + ("Category Sum of Evens", 6, 6): {0: 9692, 26: 45108, 34: 45200}, + ("Category Sum of Evens", 6, 7): {4: 1437, 26: 42351, 34: 56212}, + ("Category Sum of Evens", 6, 8): {4: 13017, 30: 51814, 36: 35169}, + ("Category Sum of Evens", 7, 1): {0: 12688, 12: 45275, 20: 42037}, + ("Category Sum of Evens", 7, 2): {0: 1433, 20: 60350, 28: 38217}, + ("Category Sum of Evens", 7, 3): {0: 13724, 24: 43514, 32: 42762}, + ("Category Sum of Evens", 7, 4): {0: 11285, 26: 40694, 34: 48021}, + ("Category Sum of Evens", 7, 5): {4: 5699, 28: 43740, 36: 50561}, + ("Category Sum of Evens", 7, 6): {4: 5478, 30: 43711, 38: 50811}, + ("Category Sum of Evens", 7, 7): {6: 9399, 32: 43251, 40: 47350}, + ("Category Sum of Evens", 7, 8): {10: 1490, 32: 40719, 40: 57791}, + ("Category Sum of Evens", 8, 1): {0: 14585, 14: 42804, 22: 42611}, + ("Category Sum of Evens", 8, 2): {0: 15891, 22: 39707, 30: 44402}, + ("Category Sum of Evens", 8, 3): {2: 297, 12: 16199, 28: 42274, 36: 41230}, + ("Category Sum of Evens", 8, 4): {0: 7625, 30: 43948, 38: 48427}, + ("Category Sum of Evens", 8, 5): {4: 413, 18: 16209, 34: 43301, 42: 40077}, + ("Category Sum of Evens", 8, 6): {6: 14927, 36: 43139, 44: 41934}, + ("Category Sum of Evens", 8, 7): {8: 5042, 36: 40440, 44: 54518}, + ("Category Sum of Evens", 8, 8): {10: 5005, 38: 44269, 46: 50726}, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 8: 33251}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 8: 55325}, + ("Category Double Threes and Fours", 1, 3): {0: 29592, 8: 70408}, + ("Category Double Threes and Fours", 1, 4): {0: 24601, 8: 75399}, + ("Category Double Threes and Fours", 1, 5): {0: 20499, 8: 79501}, + ("Category Double Threes and Fours", 1, 6): {0: 17116, 8: 82884}, + ("Category Double Threes and Fours", 1, 7): {0: 14193, 8: 85807}, + ("Category Double Threes and Fours", 1, 8): {0: 11977, 8: 88023}, + ("Category Double Threes and Fours", 2, 1): {0: 44382, 8: 55618}, + ("Category Double Threes and Fours", 2, 2): {0: 19720, 8: 57236, 14: 23044}, + ("Category Double Threes and Fours", 2, 3): {0: 8765, 8: 41937, 14: 49298}, + ("Category Double Threes and Fours", 2, 4): {0: 6164, 16: 93836}, + ("Category Double Threes and Fours", 2, 5): {0: 4307, 8: 38682, 16: 57011}, + ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 32717, 16: 64404}, + ("Category Double Threes and Fours", 2, 7): {0: 6679, 16: 93321}, + ("Category Double Threes and Fours", 2, 8): {0: 4758, 16: 95242}, + ("Category Double Threes and Fours", 3, 1): {0: 29378, 8: 50024, 14: 20598}, + ("Category Double Threes and Fours", 3, 2): {0: 8894, 14: 74049, 18: 17057}, + ("Category Double Threes and Fours", 3, 3): {0: 2643, 14: 62555, 22: 34802}, + ("Category Double Threes and Fours", 3, 4): {0: 1523, 6: 19996, 16: 50281, 22: 28200}, + ("Category Double Threes and Fours", 3, 5): {0: 845, 16: 60496, 24: 38659}, + ("Category Double Threes and Fours", 3, 6): {0: 499, 16: 51131, 24: 48370}, + ("Category Double Threes and Fours", 3, 7): {0: 5542, 16: 37755, 24: 56703}, + ("Category Double Threes and Fours", 3, 8): {0: 3805, 16: 32611, 24: 63584}, + ("Category Double Threes and Fours", 4, 1): {0: 19809, 8: 39303, 16: 40888}, + ("Category Double Threes and Fours", 4, 2): {0: 3972, 16: 71506, 22: 24522}, + ("Category Double Threes and Fours", 4, 3): {0: 745, 18: 53727, 22: 28503, 28: 17025}, + ("Category Double Threes and Fours", 4, 4): {0: 4862, 16: 34879, 22: 33529, 28: 26730}, + ("Category Double Threes and Fours", 4, 5): {0: 2891, 16: 25367, 24: 46333, 30: 25409}, + ("Category Double Threes and Fours", 4, 6): {0: 2525, 24: 62353, 30: 35122}, + ("Category Double Threes and Fours", 4, 7): {0: 1042, 24: 54543, 32: 44415}, + ("Category Double Threes and Fours", 4, 8): {0: 2510, 24: 44681, 32: 52809}, + ("Category Double Threes and Fours", 5, 1): {0: 13122, 14: 68022, 20: 18856}, + ("Category Double Threes and Fours", 5, 2): {0: 1676, 14: 37791, 22: 40810, 28: 19723}, + ("Category Double Threes and Fours", 5, 3): {0: 2945, 16: 28193, 22: 26795, 32: 42067}, + ("Category Double Threes and Fours", 5, 4): {0: 2807, 26: 53419, 30: 26733, 36: 17041}, + ("Category Double Threes and Fours", 5, 5): {0: 3651, 24: 38726, 32: 41484, 38: 16139}, + ("Category Double Threes and Fours", 5, 6): {0: 362, 12: 13070, 32: 61608, 38: 24960}, + ("Category Double Threes and Fours", 5, 7): {0: 161, 12: 15894, 32: 49464, 38: 34481}, + ("Category Double Threes and Fours", 5, 8): {0: 82, 12: 11438, 32: 45426, 40: 43054}, + ("Category Double Threes and Fours", 6, 1): {0: 8738, 6: 26451, 16: 43879, 22: 20932}, + ("Category Double Threes and Fours", 6, 2): {0: 784, 16: 38661, 28: 42164, 32: 18391}, + ("Category Double Threes and Fours", 6, 3): {0: 1062, 22: 34053, 28: 27996, 38: 36889}, + ("Category Double Threes and Fours", 6, 4): {0: 439, 12: 13100, 30: 43296, 40: 43165}, + ("Category Double Threes and Fours", 6, 5): {0: 3957, 34: 51190, 38: 26734, 44: 18119}, + ("Category Double Threes and Fours", 6, 6): {0: 4226, 32: 37492, 40: 40719, 46: 17563}, + ("Category Double Threes and Fours", 6, 7): {0: 31, 12: 13933, 40: 60102, 46: 25934}, + ("Category Double Threes and Fours", 6, 8): {8: 388, 22: 16287, 40: 48255, 48: 35070}, + ("Category Double Threes and Fours", 7, 1): {0: 5803, 8: 28280, 14: 26186, 26: 39731}, + ("Category Double Threes and Fours", 7, 2): {0: 3319, 20: 36331, 30: 38564, 36: 21786}, + ("Category Double Threes and Fours", 7, 3): {0: 2666, 18: 16444, 34: 41412, 44: 39478}, + ("Category Double Threes and Fours", 7, 4): {0: 99, 12: 9496, 38: 50302, 46: 40103}, + ("Category Double Threes and Fours", 7, 5): {0: 45, 12: 13200, 42: 52460, 50: 34295}, + ("Category Double Threes and Fours", 7, 6): {8: 2400, 28: 16653, 46: 60564, 52: 20383}, + ("Category Double Threes and Fours", 7, 7): {6: 7, 12: 11561, 44: 44119, 54: 44313}, + ("Category Double Threes and Fours", 7, 8): {8: 4625, 44: 40601, 48: 26475, 54: 28299}, + ("Category Double Threes and Fours", 8, 1): {0: 3982, 16: 56447, 28: 39571}, + ("Category Double Threes and Fours", 8, 2): {0: 1645, 20: 25350, 30: 37385, 42: 35620}, + ("Category Double Threes and Fours", 8, 3): {0: 6, 26: 23380, 40: 40181, 50: 36433}, + ("Category Double Threes and Fours", 8, 4): {0: 541, 20: 16547, 42: 38406, 52: 44506}, + ("Category Double Threes and Fours", 8, 5): {6: 2956, 30: 16449, 46: 43983, 56: 36612}, + ("Category Double Threes and Fours", 8, 6): {0: 2, 12: 7360, 38: 19332, 54: 53627, 58: 19679}, + ("Category Double Threes and Fours", 8, 7): {6: 9699, 48: 38611, 54: 28390, 60: 23300}, + ("Category Double Threes and Fours", 8, 8): {8: 5, 20: 10535, 52: 41790, 62: 47670}, + ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 8: 33433}, + ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 8: 55191}, + ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 8: 62900}, + ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 8: 69037}, + ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 8: 74684}, + ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 8: 78495}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 8: 82324}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 8: 85029}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 8: 55434}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 8: 57152, 12: 22885}, + ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 8: 52065, 16: 34169}, + ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 8: 46446, 16: 44011}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 8: 40772, 16: 52756}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 10306, 12: 46932, 16: 42762}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 7120, 12: 42245, 16: 50635}, + ("Category Quadruple Ones and Twos", 2, 8): {0: 4989, 12: 37745, 16: 57266}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 8: 50321, 16: 20239}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 8: 42729, 16: 48414}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 12: 53387, 20: 41550}, + ("Category Quadruple Ones and Twos", 3, 4): {0: 8395, 16: 64605, 24: 27000}, + ("Category Quadruple Ones and Twos", 3, 5): {0: 4895, 16: 58660, 24: 36445}, + ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 16: 52710, 24: 44609}, + ("Category Quadruple Ones and Twos", 3, 7): {0: 586, 16: 46781, 24: 52633}, + ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 16: 39406, 24: 59653}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 8: 46945, 16: 33364}, + ("Category Quadruple Ones and Twos", 4, 2): {0: 4023, 12: 50885, 24: 45092}, + ("Category Quadruple Ones and Twos", 4, 3): {0: 6553, 16: 52095, 28: 41352}, + ("Category Quadruple Ones and Twos", 4, 4): {0: 3221, 16: 41367, 24: 39881, 28: 15531}, + ("Category Quadruple Ones and Twos", 4, 5): {0: 1561, 20: 48731, 28: 49708}, + ("Category Quadruple Ones and Twos", 4, 6): {0: 190, 20: 38723, 28: 42931, 32: 18156}, + ("Category Quadruple Ones and Twos", 4, 7): {0: 5419, 24: 53017, 32: 41564}, + ("Category Quadruple Ones and Twos", 4, 8): {0: 3135, 24: 47352, 32: 49513}, + ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 8: 41252, 20: 45636}, + ("Category Quadruple Ones and Twos", 5, 2): {0: 7293, 16: 50711, 28: 41996}, + ("Category Quadruple Ones and Twos", 5, 3): {0: 719, 20: 55921, 32: 43360}, + ("Category Quadruple Ones and Twos", 5, 4): {0: 1152, 20: 38570, 32: 60278}, + ("Category Quadruple Ones and Twos", 5, 5): {0: 5647, 24: 40910, 36: 53443}, + ("Category Quadruple Ones and Twos", 5, 6): {0: 194, 28: 51527, 40: 48279}, + ("Category Quadruple Ones and Twos", 5, 7): {0: 1449, 28: 39301, 36: 41332, 40: 17918}, + ("Category Quadruple Ones and Twos", 5, 8): {0: 6781, 32: 52834, 40: 40385}, + ("Category Quadruple Ones and Twos", 6, 1): {0: 8646, 12: 53753, 24: 37601}, + ("Category Quadruple Ones and Twos", 6, 2): {0: 844, 16: 40583, 28: 58573}, + ("Category Quadruple Ones and Twos", 6, 3): {0: 1241, 24: 54870, 36: 43889}, + ("Category Quadruple Ones and Twos", 6, 4): {0: 1745, 28: 53286, 40: 44969}, + ("Category Quadruple Ones and Twos", 6, 5): {0: 2076, 32: 56909, 44: 41015}, + ("Category Quadruple Ones and Twos", 6, 6): {0: 6827, 32: 39400, 44: 53773}, + ("Category Quadruple Ones and Twos", 6, 7): {0: 1386, 36: 49865, 48: 48749}, + ("Category Quadruple Ones and Twos", 6, 8): {0: 1841, 36: 38680, 44: 40600, 48: 18879}, + ("Category Quadruple Ones and Twos", 7, 1): {0: 5780, 12: 46454, 24: 47766}, + ("Category Quadruple Ones and Twos", 7, 2): {0: 6122, 20: 38600, 32: 55278}, + ("Category Quadruple Ones and Twos", 7, 3): {0: 2065, 28: 52735, 40: 45200}, + ("Category Quadruple Ones and Twos", 7, 4): {0: 1950, 32: 50270, 44: 47780}, + ("Category Quadruple Ones and Twos", 7, 5): {0: 2267, 36: 49235, 48: 48498}, + ("Category Quadruple Ones and Twos", 7, 6): {0: 2500, 40: 53934, 52: 43566}, + ("Category Quadruple Ones and Twos", 7, 7): {0: 6756, 44: 53730, 56: 39514}, + ("Category Quadruple Ones and Twos", 7, 8): {0: 3625, 44: 45159, 56: 51216}, + ("Category Quadruple Ones and Twos", 8, 1): {0: 11493, 16: 50043, 28: 38464}, + ("Category Quadruple Ones and Twos", 8, 2): {0: 136, 24: 47795, 36: 52069}, + ("Category Quadruple Ones and Twos", 8, 3): {0: 2744, 32: 51640, 48: 45616}, + ("Category Quadruple Ones and Twos", 8, 4): {0: 2293, 36: 45979, 48: 51728}, + ("Category Quadruple Ones and Twos", 8, 5): {0: 2181, 40: 44909, 52: 52910}, + ("Category Quadruple Ones and Twos", 8, 6): {4: 2266, 44: 44775, 56: 52959}, + ("Category Quadruple Ones and Twos", 8, 7): {8: 2344, 48: 50198, 60: 47458}, + ("Category Quadruple Ones and Twos", 8, 8): {8: 2808, 48: 37515, 56: 37775, 64: 21902}, ("Category Micro Straight", 1, 1): {0: 100000}, ("Category Micro Straight", 1, 2): {0: 100000}, ("Category Micro Straight", 1, 3): {0: 100000}, @@ -3527,7 +2329,7 @@ ("Category 4&5 Full House", 4, 6): {0: 100000}, ("Category 4&5 Full House", 4, 7): {0: 100000}, ("Category 4&5 Full House", 4, 8): {0: 100000}, - ("Category 4&5 Full House", 5, 1): {0: 99724, 50: 276}, + ("Category 4&5 Full House", 5, 1): {0: 100000}, ("Category 4&5 Full House", 5, 2): {0: 96607, 50: 3393}, ("Category 4&5 Full House", 5, 3): {0: 88788, 50: 11212}, ("Category 4&5 Full House", 5, 4): {0: 77799, 50: 22201}, @@ -3535,7 +2337,7 @@ ("Category 4&5 Full House", 5, 6): {0: 54548, 50: 45452}, ("Category 4&5 Full House", 5, 7): {0: 44898, 50: 55102}, ("Category 4&5 Full House", 5, 8): {0: 36881, 50: 63119}, - ("Category 4&5 Full House", 6, 1): {0: 98841, 50: 1159}, + ("Category 4&5 Full House", 6, 1): {0: 100000}, ("Category 4&5 Full House", 6, 2): {0: 88680, 50: 11320}, ("Category 4&5 Full House", 6, 3): {0: 70215, 50: 29785}, ("Category 4&5 Full House", 6, 4): {0: 50801, 50: 49199}, diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index c36c59544f15..3a79eff04046 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.1" + ap_world_version = "2.1.2" def _get_yachtdice_data(self): return { @@ -190,7 +190,6 @@ def generate_early(self): if self.frags_per_roll == 1: self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory else: - self.itempool.append("Roll") # always add a full roll to make generation easier (will be early) self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add) already_items = len(self.itempool) @@ -231,13 +230,10 @@ def generate_early(self): weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll - extra_points_added = 0 - multipliers_added = 0 - items_added = 0 - - def get_item_to_add(weights, extra_points_added, multipliers_added, items_added): - items_added += 1 + extra_points_added = [0] # make it a mutible type so we can change the value in the function + step_score_multipliers_added = [0] + def get_item_to_add(weights, extra_points_added, step_score_multipliers_added): all_items = self.itempool + self.precollected dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment") if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice: @@ -246,21 +242,18 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll: weights["Roll"] = 0 # don't allow >= 6 rolls - # Don't allow too many multipliers - if multipliers_added > 50: - weights["Fixed Score Multiplier"] = 0 - weights["Step Score Multiplier"] = 0 - # Don't allow too many extra points - if extra_points_added > 300: + if extra_points_added[0] > 400: weights["Points"] = 0 + if step_score_multipliers_added[0] > 10: + weights["Step Score Multiplier"] = 0 + # if all weights are zero, allow to add fixed score multiplier, double category, points. if sum(weights.values()) == 0: - if multipliers_added <= 50: - weights["Fixed Score Multiplier"] = 1 + weights["Fixed Score Multiplier"] = 1 weights["Double category"] = 1 - if extra_points_added <= 300: + if extra_points_added[0] <= 400: weights["Points"] = 1 # Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item @@ -274,11 +267,10 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) return "Roll" if self.frags_per_roll == 1 else "Roll Fragment" elif which_item_to_add == "Fixed Score Multiplier": weights["Fixed Score Multiplier"] /= 1.05 - multipliers_added += 1 return "Fixed Score Multiplier" elif which_item_to_add == "Step Score Multiplier": weights["Step Score Multiplier"] /= 1.1 - multipliers_added += 1 + step_score_multipliers_added[0] += 1 return "Step Score Multiplier" elif which_item_to_add == "Double category": # Below entries are the weights to add each category. @@ -303,15 +295,15 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0] if choice == "1 Point": weights["Points"] /= 1.01 - extra_points_added += 1 + extra_points_added[0] += 1 return "1 Point" elif choice == "10 Points": weights["Points"] /= 1.1 - extra_points_added += 10 + extra_points_added[0] += 10 return "10 Points" elif choice == "100 Points": weights["Points"] /= 2 - extra_points_added += 100 + extra_points_added[0] += 100 return "100 Points" else: raise Exception("Unknown point value (Yacht Dice)") @@ -320,7 +312,7 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) # adding 17 items as a start seems like the smartest way to get close to 1000 points for _ in range(17): - self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added)) + self.itempool.append(get_item_to_add(weights, extra_points_added, step_score_multipliers_added)) score_in_logic = dice_simulation_fill_pool( self.itempool + self.precollected, @@ -348,7 +340,7 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) else: # Keep adding items until a score of 1000 is in logic while score_in_logic < 1000: - item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added) + item_to_add = get_item_to_add(weights, extra_points_added, step_score_multipliers_added) self.itempool.append(item_to_add) if item_to_add == "1 Point": score_in_logic += 1 From 430b71a092b9bff8aa8f6a61dd4266a14cd056ec Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 7 Sep 2024 17:03:04 -0500 Subject: [PATCH 231/393] Core: have webhost slot name links go through the launcher (#2779) * Core: have webhost slot name links go through the launcher so that components can use them * fix query handling, remove debug prints, and change mousover text for new behavior * remove a missed debug and unused function * filter room id to suuid since that's what everything else uses * pass args to common client correctly * add GUI to select which client to open * remove args parsing and "require" components to parse it themselves * support for messenger since it was basically already done * use "proper" args argparsing and clean up uri handling * use a timer and auto launch text client if no component is found * change the timer to be a bit more appealing. also found a bug lmao * don't hold 5 hostage and capitalize URI ig --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 4 +- Launcher.py | 98 +++++++++++++++++++++++++++++--- WebHostLib/templates/macros.html | 2 +- inno_setup.iss | 4 +- worlds/LauncherComponents.py | 15 +++-- worlds/messenger/__init__.py | 2 +- worlds/messenger/client_setup.py | 15 +++-- 7 files changed, 115 insertions(+), 25 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 750bee80bd70..fe9df38dbdeb 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -994,7 +994,7 @@ def get_base_parser(description: typing.Optional[str] = None): return parser -def run_as_textclient(): +def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry tags = CommonContext.tags | {"TextOnly"} @@ -1033,7 +1033,7 @@ async def main(args): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args() + args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable if args.url: url = urllib.parse.urlparse(args.url) diff --git a/Launcher.py b/Launcher.py index 6b66b2a3a671..97903e2ad103 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,10 +16,11 @@ import shlex import subprocess import sys +import urllib.parse import webbrowser from os.path import isfile from shutil import which -from typing import Callable, Sequence, Union, Optional +from typing import Callable, Optional, Sequence, Tuple, Union import Utils import settings @@ -107,7 +108,81 @@ def update_settings(): ]) -def identify(path: Union[None, str]): +def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: + url = urllib.parse.urlparse(path) + queries = urllib.parse.parse_qs(url.query) + launch_args = (path, *launch_args) + client_component = None + text_client_component = None + if "game" in queries: + game = queries["game"][0] + else: # TODO around 0.6.0 - this is for pre this change webhost uri's + game = "Archipelago" + for component in components: + if component.supports_uri and component.game_name == game: + client_component = component + elif component.display_name == "Text Client": + text_client_component = component + + from kvui import App, Button, BoxLayout, Label, Clock, Window + + class Popup(App): + timer_label: Label + remaining_time: Optional[int] + + def __init__(self): + self.title = "Connect to Multiworld" + self.icon = r"data/icon.png" + super().__init__() + + def build(self): + layout = BoxLayout(orientation="vertical") + + if client_component is None: + self.remaining_time = 7 + label_text = (f"A game client able to parse URIs was not detected for {game}.\n" + f"Launching Text Client in 7 seconds...") + self.timer_label = Label(text=label_text) + layout.add_widget(self.timer_label) + Clock.schedule_interval(self.update_label, 1) + else: + layout.add_widget(Label(text="Select client to open and connect with.")) + button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) + + text_client_button = Button( + text=text_client_component.display_name, + on_release=lambda *args: run_component(text_client_component, *launch_args) + ) + button_row.add_widget(text_client_button) + + game_client_button = Button( + text=client_component.display_name, + on_release=lambda *args: run_component(client_component, *launch_args) + ) + button_row.add_widget(game_client_button) + + layout.add_widget(button_row) + + return layout + + def update_label(self, dt): + if self.remaining_time > 1: + # countdown the timer and string replace the number + self.remaining_time -= 1 + self.timer_label.text = self.timer_label.text.replace( + str(self.remaining_time + 1), str(self.remaining_time) + ) + else: + # our timer is finished so launch text client and close down + run_component(text_client_component, *launch_args) + Clock.unschedule(self.update_label) + App.get_running_app().stop() + Window.close() + + Popup().run() + + +def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]: if path is None: return None, None for component in components: @@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): elif not args: args = {} - if args.get("Patch|Game|Component", None) is not None: - file, component = identify(args["Patch|Game|Component"]) + path = args.get("Patch|Game|Component|url", None) + if path is not None: + if path.startswith("archipelago://"): + handle_uri(path, args.get("args", ())) + return + file, component = identify(path) if file: args['file'] = file if component: args['component'] = component if not component: - logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}") + logging.warning(f"Could not identify Component responsible for {path}") if args["update_settings"]: update_settings() - if 'file' in args: + if "file" in args: run_component(args["component"], args["file"], *args["args"]) - elif 'component' in args: + elif "component" in args: run_component(args["component"], *args["args"]) elif not args["update_settings"]: run_gui() @@ -326,8 +405,9 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") - run_group.add_argument("Patch|Game|Component", type=str, nargs="?", - help="Pass either a patch file, a generated game or the name of a component to run.") + run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?", + help="Pass either a patch file, a generated game, the component name to run, or a url to " + "connect with.") run_group.add_argument("args", nargs="*", help="Arguments to pass to component.") main(parser.parse_args()) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 7bbb894de090..6b2a4b0ed784 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} -
{{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.data %} diff --git a/inno_setup.iss b/inno_setup.iss index 3bb76fc40abe..38e655d917c1 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; -Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; -Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; +Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; +Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; [Code] // See: https://stackoverflow.com/a/51614652/2287576 diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index d127bbea36ed..4c64642abacb 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -26,10 +26,13 @@ class Component: cli: bool func: Optional[Callable] file_identifier: Optional[Callable[[str], bool]] + game_name: Optional[str] + supports_uri: Optional[bool] def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None, - func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None): + func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None, + game_name: Optional[str] = None, supports_uri: Optional[bool] = False): self.display_name = display_name self.script_name = script_name self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None @@ -45,6 +48,8 @@ def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_ Type.ADJUSTER if "Adjuster" in display_name else Type.MISC) self.func = func self.file_identifier = file_identifier + self.game_name = game_name + self.supports_uri = supports_uri def handles_file(self, path: str): return self.file_identifier(path) if self.file_identifier else False @@ -56,10 +61,10 @@ def __repr__(self): processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()): global processes import multiprocessing - process = multiprocessing.Process(target=func, name=name) + process = multiprocessing.Process(target=func, name=name, args=args) process.start() processes.add(process) @@ -78,9 +83,9 @@ def __call__(self, path: str) -> bool: return False -def launch_textclient(): +def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, name="TextClient") + launch_subprocess(CommonClient.run_as_textclient, "TextClient", args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index a03c33c2f7b6..1bca3a37ad71 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -19,7 +19,7 @@ from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation components.append( - Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True) + Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True) ) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 9fd08e52d899..6bff78df364d 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -1,3 +1,4 @@ +import argparse import io import logging import os.path @@ -17,7 +18,7 @@ MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" -def launch_game(url: Optional[str] = None) -> None: +def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" @@ -150,15 +151,19 @@ def available_mod_update(latest_version: str) -> bool: install_mod() elif should_update is None: return + + parser = argparse.ArgumentParser(description="Messenger Client Launcher") + parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") + args = parser.parse_args(args) if not is_windows: - if url: - open_file(f"steam://rungameid/764790//{url}/") + if args.url: + open_file(f"steam://rungameid/764790//{args.url}/") else: open_file("steam://rungameid/764790") else: os.chdir(game_folder) - if url: - subprocess.Popen([MessengerWorld.settings.game_path, str(url)]) + if args.url: + subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)]) else: subprocess.Popen(MessengerWorld.settings.game_path) os.chdir(working_directory) From b8c2e14e8b0b1f7837b7cefc1aaeb94ce87bf93f Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 07:17:20 -0500 Subject: [PATCH 232/393] CommonClient: allow worlds to change title of run_gui without rewriting it (#3297) * moves the title name in CommonContext.run_gui into a parameter defaulted to the normal default so others using it don't have to rewrite everything * Change to using a GameManager attribute instead of a default param * Update CommonClient.py treble suggestion 1 Co-authored-by: Aaron Wagener * Update CommonClient.py treble suggestion 2 Co-authored-by: Aaron Wagener * Update CommonClient.py treble suggestion 3 Co-authored-by: Doug Hoskisson * Use make_gui() instead of a property to push kivy importing back to lazy loading regardless of gui_enabled status * cleanup * almost forgot to type it * change make_gui to be a class so clients can subclass it * clean up code readability --------- Co-authored-by: Aaron Wagener Co-authored-by: Doug Hoskisson Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index fe9df38dbdeb..7f91172acf6c 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,17 +662,19 @@ def handle_connection_loss(self, msg: str) -> None: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def run_gui(self): - """Import kivy UI system and start running it as self.ui_task.""" + def make_gui(self) -> type: + """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager class TextManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] base_title = "Archipelago Text Client" - self.ui = TextManager(self) + return TextManager + + def run_gui(self): + """Import kivy UI system from make_gui() and start running it as self.ui_task.""" + ui_class = self.make_gui() + self.ui = ui_class(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") def run_cli(self): From 5348f693fe9edd4756b91969a0ac66f5877fc4be Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Sep 2024 05:19:37 -0700 Subject: [PATCH 233/393] Pokemon Emerald: Use some new state functions, improve rule reuse (#3383) * Pokemon Emerald: Use some new state functions, improve rule reuse * Pokemon Emerald: Remove a couple more extra lambdas * Pokemon Emerald: Swap some rules to use exclusive groups/lists * Pokemon Emerald: Linting We're not gonna keep both me and the linter happy here, but this at least gets things more consistent * Pokemon Emerald: Update _exclusive to _unique --- worlds/pokemon_emerald/rules.py | 159 ++++++++++++++++---------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 5b2aaa1ffcd0..5f83686ebeec 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -19,20 +19,20 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: hm_rules: Dict[str, Callable[[CollectionState], bool]] = {} for hm, badges in world.hm_requirements.items(): if isinstance(badges, list): - hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \ - and state.has_all(badges, world.player) + hm_rules[hm] = lambda state, hm=hm, badges=badges: \ + state.has(hm, world.player) and state.has_all(badges, world.player) else: - hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \ - and state.has_group("Badges", world.player, badges) + hm_rules[hm] = lambda state, hm=hm, badges=badges: \ + state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges) def has_acro_bike(state: CollectionState): return state.has("Acro Bike", world.player) def has_mach_bike(state: CollectionState): return state.has("Mach Bike", world.player) - + def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: - return sum([state.has(event, world.player) for event in [ + return state.has_from_list_unique([ "EVENT_DEFEAT_ROXANNE", "EVENT_DEFEAT_BRAWLY", "EVENT_DEFEAT_WATTSON", @@ -41,7 +41,7 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: "EVENT_DEFEAT_WINONA", "EVENT_DEFEAT_TATE_AND_LIZA", "EVENT_DEFEAT_JUAN", - ]]) >= n + ], world.player, n) huntable_legendary_events = [ f"EVENT_ENCOUNTER_{key}" @@ -61,8 +61,9 @@ def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: }.items() if name in world.options.allowed_legendary_hunt_encounters.value ] + def encountered_n_legendaries(state: CollectionState, n: int) -> bool: - return sum(int(state.has(event, world.player)) for event in huntable_legendary_events) >= n + return state.has_from_list_unique(huntable_legendary_events, world.player, n) def get_entrance(entrance: str): return world.multiworld.get_entrance(entrance, world.player) @@ -235,11 +236,11 @@ def get_location(location: str): if world.options.norman_requirement == NormanRequirement.option_badges: set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"), - lambda state: state.has_group("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) ) set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"), - lambda state: state.has_group("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) ) else: set_rule( @@ -299,15 +300,15 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_116_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_116_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_116_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_116_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Rusturf Tunnel @@ -347,19 +348,19 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_115_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_115_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_115_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_115_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) if world.options.extra_boulders: @@ -375,7 +376,7 @@ def get_location(location: str): if world.options.extra_bumpy_slope: set_rule( get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"), - lambda state: has_acro_bike(state) + has_acro_bike ) else: set_rule( @@ -386,17 +387,17 @@ def get_location(location: str): # Route 105 set_rule( get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_105_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_105_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_105_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_105_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"), @@ -439,7 +440,7 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"), - lambda state: has_mach_bike(state) + has_mach_bike ) # Route 107 @@ -643,15 +644,15 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_114_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_114_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_114_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_114_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Meteor Falls @@ -699,11 +700,11 @@ def get_location(location: str): # Jagged Pass set_rule( get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"), @@ -719,11 +720,11 @@ def get_location(location: str): # Mirage Tower set_rule( get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"), @@ -812,15 +813,15 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_118_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_118_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_118_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_118_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Route 119 @@ -830,11 +831,11 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"), @@ -850,7 +851,7 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"), - lambda state: has_acro_bike(state) + has_acro_bike ) if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value: set_rule( @@ -927,11 +928,11 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"), @@ -1115,17 +1116,17 @@ def get_location(location: str): # Route 125 set_rule( get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_125_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_125_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_125_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_125_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Shoal Cave @@ -1257,17 +1258,17 @@ def get_location(location: str): ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_127_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_127_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_127_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_127_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Route 128 @@ -1374,17 +1375,17 @@ def get_location(location: str): # Route 129 set_rule( get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_129_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_129_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_129_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_129_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Pacifidlog Town @@ -1505,7 +1506,7 @@ def get_location(location: str): if world.options.elite_four_requirement == EliteFourRequirement.option_badges: set_rule( get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"), - lambda state: state.has_group("Badges", world.player, world.options.elite_four_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value) ) else: set_rule( From a6521084723c2b9702961d7cee97dcef96165918 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:21:26 -0400 Subject: [PATCH 234/393] Docs: Update Trap classification comment #3485 --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 715732589b67..b40b872f0c8c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1207,7 +1207,7 @@ class ItemClassification(IntFlag): filler = 0b0000 # aka trash, as in filler items like ammo, currency etc, progression = 0b0001 # Item that is logically relevant useful = 0b0010 # Item that is generally quite useful, but not required for anything logical - trap = 0b0100 # detrimental or entirely useless (nothing) item + trap = 0b0100 # detrimental item skip_balancing = 0b1000 # should technically never occur on its own # Item that is logically relevant, but progression balancing should not touch. # Typically currency or other counted items. From dad228cd4a760d2d49706d4026791bc3d0e4f377 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 8 Sep 2024 08:42:59 -0400 Subject: [PATCH 235/393] TUNIC: Logic Rules Redux (#3544) * Clean these functions up, get the hell out of here 5 parameter function * Clean up a bunch of rules that no longer need to be multi-lined since the functions are shorter * Clean up some range functions * Update to use world instead of player like Vi recommended * Fix merge conflict * Create new options * Slightly revise ls rule * Update options.py * Update options.py * Add tedious option for ls * Update laurels zips description * Create new options * Slightly revise ls rule * Update options.py * Update options.py * Add tedious option for ls * Update laurels zips description * Creating structures to redo ladder storage rules * Put together overworld ladder groups, remove tedious * Write up the rules for the regular rules * Update slot data and UT stuff * Put new ice grapple stuff in er rules * Ice grapple hard to get to fountain cross room * More ladder data * Wrote majority of overworld ladder rules * Finish the ladder storage rules * Update notes * Add note * Add well rail to the rules * More rules * Comment out logically irrelevant entrances * Update with laurels_zip helper * Add parameter to has_ice_grapple_logic for difficulty * Add new parameter to has_ice_grapple_logic * Move ice grapple chest to lower forest in ER/ladders * Fix rule * Finishing out hooking the new rules into the code * Fix bugs * Add more hard ice grapples * Fix more bugs * Shops my beloved * Change victory condition back * Remove debug stuff * Update plando connections description * Fix extremely rare bug * Add well front -> back hard ladder storages * Note in ls rules about knocking yourself down with bombs being out of logic * Add atoll fuse with wand + hard ls * Add some nonsense that boils down to activating the fuse in overworld * Further update LS description * Fix missing logic on bridge switch chest in upper zig * Revise upper zig rule change to account for ER * Fix merge conflict * Fix formatting, fix rule for heir access after merge * Add the shop sword logic stuff in * Remove todo that was already done * Fill out a to-do with some cursed nonsense * Fix event in wrong region * Fix missing cathedral -> elevator connection * Fix missing cathedral -> elevator connection * Add ER exception to cathedral -> elevator * Fix secret gathering place issue * Fix incorrect ls rule * Move 3 locations to Quarry Back since they're easily accessible from the back * Also update non-er region * Remove redundant parentheses * Add new test for a weird edge case in ER * Slight option description updates * Use has_ladder in spots where it wasn't used for some reason, add a comment * Fix unit test for ER * Update per exempt's suggestion * Add back LogicRules as an invisible option, to not break old yamls * Remove unused elevation from portal class * Update ladder storage without items description * Remove shop_scene stuff since it's no longer relevant in the mod by the time this version comes out * Remove shop scene stuff from game info since it's no longer relevant in the mod by the time this comes out * Update portal list to match main * god I love github merging things * Remove note * Add ice grapple hard path from upper overworld to temple rafters entrance * Actually that should be medium * Remove outdated note * Add ice grapple hard for swamp mid to the ledge * Add missing laurels zip in swamp * Some fixes to the ladder storage data while reviewing it * Add unit test for weird edge case * Backport outlet region system to fix ls bug * Fix incorrect ls, add todo * Add missing swamp ladder storage connections * Add swamp zip to er data * Add swamp zip to er rules * Add hard ice grapple for forest grave path main to upper * Add ice grapple logic for all bomb walls except the east quarry one * Add ice grapple logic for frog stairs eye to mouth without the ladder * Add hard ice grapple for overworld to the stairs to west garden * Add the ice grapple boss quick kills to medium ice grappling * Add the reverse connection for the ice grapple kill on Garden Knight * Add atoll house ice grapple push, and add west garden ice grapple entry to the regular rules --- worlds/tunic/__init__.py | 55 +- worlds/tunic/docs/en_TUNIC.md | 2 - worlds/tunic/er_data.py | 337 +++++++----- worlds/tunic/er_rules.py | 759 ++++++++++++---------------- worlds/tunic/er_scripts.py | 114 +++-- worlds/tunic/items.py | 2 + worlds/tunic/ladder_storage_data.py | 186 +++++++ worlds/tunic/locations.py | 6 +- worlds/tunic/options.py | 124 +++-- worlds/tunic/regions.py | 3 +- worlds/tunic/rules.py | 114 +++-- worlds/tunic/test/test_access.py | 54 ++ 12 files changed, 1050 insertions(+), 706 deletions(-) create mode 100644 worlds/tunic/ladder_storage_data.py diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index bbffd9c1440e..cdd968acce44 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -7,8 +7,9 @@ from .er_rules import set_er_location_rules from .regions import tunic_regions from .er_scripts import create_er_regions -from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections +from .er_data import portal_mapping, RegionInfo, tunic_er_regions +from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, + LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage) from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -48,10 +49,12 @@ class TunicLocation(Location): class SeedGroup(TypedDict): - logic_rules: int # logic rules value + laurels_zips: bool # laurels_zips value + ice_grappling: int # ice_grappling value + ladder_storage: int # ls value laurels_at_10_fairies: bool # laurels location value fixed_shop: bool # fixed shop value - plando: TunicPlandoConnections # consolidated of plando connections for the seed group + plando: TunicPlandoConnections # consolidated plando connections for the seed group class TunicWorld(World): @@ -77,8 +80,17 @@ class TunicWorld(World): tunic_portal_pairs: Dict[str, str] er_portal_hints: Dict[int, str] seed_groups: Dict[str, SeedGroup] = {} + shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected + er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work def generate_early(self) -> None: + if self.options.logic_rules >= LogicRules.option_no_major_glitches: + self.options.laurels_zips.value = LaurelsZips.option_true + self.options.ice_grappling.value = IceGrappling.option_medium + if self.options.logic_rules.value == LogicRules.option_unrestricted: + self.options.ladder_storage.value = LadderStorage.option_medium + + self.er_regions = tunic_er_regions.copy() if self.options.plando_connections: for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later @@ -99,7 +111,10 @@ def generate_early(self) -> None: self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"] self.options.sword_progression.value = passthrough["sword_progression"] self.options.ability_shuffling.value = passthrough["ability_shuffling"] - self.options.logic_rules.value = passthrough["logic_rules"] + self.options.laurels_zips.value = passthrough["laurels_zips"] + self.options.ice_grappling.value = passthrough["ice_grappling"] + self.options.ladder_storage.value = passthrough["ladder_storage"] + self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"] self.options.lanternless.value = passthrough["lanternless"] self.options.maskless.value = passthrough["maskless"] self.options.hexagon_quest.value = passthrough["hexagon_quest"] @@ -118,19 +133,28 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: group = tunic.options.entrance_rando.value # if this is the first world in the group, set the rules equal to its rules if group not in cls.seed_groups: - cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value, - laurels_at_10_fairies=tunic.options.laurels_location == 3, - fixed_shop=bool(tunic.options.fixed_shop), - plando=tunic.options.plando_connections) + cls.seed_groups[group] = \ + SeedGroup(laurels_zips=bool(tunic.options.laurels_zips), + ice_grappling=tunic.options.ice_grappling.value, + ladder_storage=tunic.options.ladder_storage.value, + laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, + fixed_shop=bool(tunic.options.fixed_shop), + plando=tunic.options.plando_connections) continue - + + # off is more restrictive + if not tunic.options.laurels_zips: + cls.seed_groups[group]["laurels_zips"] = False + # lower value is more restrictive + if tunic.options.ice_grappling < cls.seed_groups[group]["ice_grappling"]: + cls.seed_groups[group]["ice_grappling"] = tunic.options.ice_grappling.value # lower value is more restrictive - if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]: - cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value + if tunic.options.ladder_storage.value < cls.seed_groups[group]["ladder_storage"]: + cls.seed_groups[group]["ladder_storage"] = tunic.options.ladder_storage.value # laurels at 10 fairies changes logic for secret gathering place placement if tunic.options.laurels_location == 3: cls.seed_groups[group]["laurels_at_10_fairies"] = True - # fewer shops, one at windmill + # more restrictive, overrides the option for others in the same group, which is better than failing imo if tunic.options.fixed_shop: cls.seed_groups[group]["fixed_shop"] = True @@ -366,7 +390,10 @@ def fill_slot_data(self) -> Dict[str, Any]: "ability_shuffling": self.options.ability_shuffling.value, "hexagon_quest": self.options.hexagon_quest.value, "fool_traps": self.options.fool_traps.value, - "logic_rules": self.options.logic_rules.value, + "laurels_zips": self.options.laurels_zips.value, + "ice_grappling": self.options.ice_grappling.value, + "ladder_storage": self.options.ladder_storage.value, + "ladder_storage_without_items": self.options.ladder_storage_without_items.value, "lanternless": self.options.lanternless.value, "maskless": self.options.maskless.value, "entrance_rando": int(bool(self.options.entrance_rando.value)), diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 27df4ce38be4..b2e1a71897c0 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -83,8 +83,6 @@ Notes: - 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 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. diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 6316292e564e..343bf3055378 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,6 +1,9 @@ -from typing import Dict, NamedTuple, List +from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional from enum import IntEnum +if TYPE_CHECKING: + from . import TunicWorld + class Portal(NamedTuple): name: str # human-readable name @@ -9,6 +12,8 @@ class Portal(NamedTuple): tag: str # vanilla tag def scene(self) -> str: # the actual scene name in Tunic + if self.region.startswith("Shop"): + return tunic_er_regions["Shop"].game_scene return tunic_er_regions[self.region].game_scene def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod @@ -458,7 +463,7 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Cathedral Main Exit", region="Cathedral", destination="Swamp Redux 2", tag="_main"), - Portal(name="Cathedral Elevator", region="Cathedral", + Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet", destination="Cathedral Arena", tag="_"), Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", destination="Swamp Redux 2", tag="_secret"), @@ -517,6 +522,13 @@ def destination_scene(self) -> str: # the vanilla connection class RegionInfo(NamedTuple): game_scene: str # the name of the scene in the actual game dead_end: int = 0 # if a region has only one exit + outlet_region: Optional[str] = None + is_fake_region: bool = False + + +# gets the outlet region name if it exists, the region if it doesn't +def get_portal_outlet_region(portal: Portal, world: "TunicWorld") -> str: + return world.er_regions[portal.region].outlet_region or portal.region class DeadEnd(IntEnum): @@ -558,11 +570,11 @@ class DeadEnd(IntEnum): "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux", outlet_region="Overworld"), "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Cube Cave Entrance Region": RegionInfo("Overworld Redux"), # other side of the bomb wall + "Overworld Town Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Cube Cave Entrance Region": RegionInfo("Overworld Redux", outlet_region="Overworld"), # other side of the bomb wall "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats), "Windmill": RegionInfo("Windmill"), "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door @@ -591,7 +603,7 @@ class DeadEnd(IntEnum): "Forest Belltower Lower": RegionInfo("Forest Belltower"), "East Forest": RegionInfo("East Forest Redux"), "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), - "East Forest Portal": RegionInfo("East Forest Redux"), + "East Forest Portal": RegionInfo("East Forest Redux", outlet_region="East Forest"), "Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), @@ -601,7 +613,7 @@ class DeadEnd(IntEnum): "Forest Grave Path Main": RegionInfo("Sword Access"), "Forest Grave Path Upper": RegionInfo("Sword Access"), "Forest Grave Path by Grave": RegionInfo("Sword Access"), - "Forest Hero's Grave": RegionInfo("Sword Access"), + "Forest Hero's Grave": RegionInfo("Sword Access", outlet_region="Forest Grave Path by Grave"), "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits "Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder "Dark Tomb Main": RegionInfo("Crypt Redux"), @@ -614,18 +626,19 @@ class DeadEnd(IntEnum): "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests "West Garden": RegionInfo("Archipelagos Redux"), "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats), - "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"), + "West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"), "West Garden after Boss": RegionInfo("Archipelagos Redux"), - "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"), "Ruined Atoll": RegionInfo("Atoll Redux"), "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), "Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), "Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"), - "Ruined Atoll Portal": RegionInfo("Atoll Redux"), - "Ruined Atoll Statue": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), + "Ruined Atoll Statue": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), "Frog Stairs Eye Exit": RegionInfo("Frog Stairs"), "Frog Stairs Upper": RegionInfo("Frog Stairs"), "Frog Stairs Lower": RegionInfo("Frog Stairs"), @@ -633,18 +646,20 @@ class DeadEnd(IntEnum): "Frog's Domain Entry": RegionInfo("frog cave main"), "Frog's Domain": RegionInfo("frog cave main"), "Frog's Domain Back": RegionInfo("frog cave main"), - "Library Exterior Tree Region": RegionInfo("Library Exterior"), + "Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"), + "Library Exterior by Tree": RegionInfo("Library Exterior"), "Library Exterior Ladder Region": RegionInfo("Library Exterior"), "Library Hall Bookshelf": RegionInfo("Library Hall"), "Library Hall": RegionInfo("Library Hall"), - "Library Hero's Grave Region": RegionInfo("Library Hall"), + "Library Hero's Grave Region": RegionInfo("Library Hall", outlet_region="Library Hall"), "Library Hall to Rotunda": RegionInfo("Library Hall"), "Library Rotunda to Hall": RegionInfo("Library Rotunda"), "Library Rotunda": RegionInfo("Library Rotunda"), "Library Rotunda to Lab": RegionInfo("Library Rotunda"), "Library Lab": RegionInfo("Library Lab"), "Library Lab Lower": RegionInfo("Library Lab"), - "Library Portal": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"), + "Library Lab on Portal Pad": RegionInfo("Library Lab"), "Library Lab to Librarian": RegionInfo("Library Lab"), "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats), "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), @@ -663,22 +678,22 @@ class DeadEnd(IntEnum): "Fortress Grave Path": RegionInfo("Fortress Reliquary"), "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted), "Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"), - "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"), "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats), "Fortress Arena": RegionInfo("Fortress Arena"), - "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"), "Lower Mountain": RegionInfo("Mountain"), "Lower Mountain Stairs": RegionInfo("Mountain"), "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats), "Quarry Connector": RegionInfo("Darkwoods Tunnel"), "Quarry Entry": RegionInfo("Quarry Redux"), "Quarry": RegionInfo("Quarry Redux"), - "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux", outlet_region="Quarry Entry"), "Quarry Back": RegionInfo("Quarry Redux"), "Quarry Monastery Entry": RegionInfo("Quarry Redux"), "Monastery Front": RegionInfo("Monastery"), "Monastery Back": RegionInfo("Monastery"), - "Monastery Hero's Grave Region": RegionInfo("Monastery"), + "Monastery Hero's Grave Region": RegionInfo("Monastery", outlet_region="Monastery Back"), "Monastery Rope": RegionInfo("Quarry Redux"), "Lower Quarry": RegionInfo("Quarry Redux"), "Even Lower Quarry": RegionInfo("Quarry Redux"), @@ -691,19 +706,21 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side - "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on - "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side - "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), + "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, for use with fixed shop on + "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side + "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"), + "Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"), "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), "Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south "Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door "Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door - "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2", outlet_region="Swamp Ledge under Cathedral Door"), # just the door "Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance - "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"), + "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"), "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats), "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), @@ -711,10 +728,10 @@ class DeadEnd(IntEnum): "Far Shore": RegionInfo("Transit"), "Far Shore to Spawn Region": RegionInfo("Transit"), "Far Shore to East Forest Region": RegionInfo("Transit"), - "Far Shore to Quarry Region": RegionInfo("Transit"), - "Far Shore to Fortress Region": RegionInfo("Transit"), - "Far Shore to Library Region": RegionInfo("Transit"), - "Far Shore to West Garden Region": RegionInfo("Transit"), + "Far Shore to Quarry Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Fortress Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Library Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to West Garden Region": RegionInfo("Transit", outlet_region="Far Shore"), "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), @@ -728,6 +745,16 @@ class DeadEnd(IntEnum): } +# this is essentially a pared down version of the region connections in rules.py, with some minor differences +# the main purpose of this is to make it so that you can access every region +# most items are excluded from the rules here, since we can assume Archipelago will properly place them +# laurels (hyperdash) can be locked at 10 fairies, requiring access to secret gathering place +# so until secret gathering place has been paired, you do not have hyperdash, so you cannot use hyperdash entrances +# Zip means you need the laurels zips option enabled +# IG# refers to ice grappling difficulties +# LS# refers to ladder storage difficulties +# LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic +# this is because it just means you can reach the entrances in that region via ladder storage traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld": { "Overworld Beach": @@ -735,13 +762,13 @@ class DeadEnd(IntEnum): "Overworld to Atoll Upper": [["Hyperdash"]], "Overworld Belltower": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Upper Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Lower Entry": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Well Ladder": [], "Overworld Ruined Passage Door": @@ -759,11 +786,11 @@ class DeadEnd(IntEnum): "Overworld after Envoy": [], "Overworld Quarry Entry": - [["NMG"]], + [["IG2"], ["LS1"]], "Overworld Tunnel Turret": - [["NMG"], ["Hyperdash"]], + [["IG1"], ["LS1"], ["Hyperdash"]], "Overworld Temple Door": - [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]], + [["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]], "Overworld Southeast Cross Door": [], "Overworld Fountain Cross Door": @@ -773,25 +800,28 @@ class DeadEnd(IntEnum): "Overworld Spawn Portal": [], "Overworld Well to Furnace Rail": - [["UR"]], + [["LS2"]], "Overworld Old House Door": [], "Cube Cave Entrance Region": [], + # drop a rudeling, icebolt or ice bomb + "Overworld to West Garden from Furnace": + [["IG3"]], }, "East Overworld": { "Above Ruined Passage": [], "After Ruined Passage": - [["NMG"]], - "Overworld": - [], + [["IG1"], ["LS1"]], + # "Overworld": + # [], "Overworld at Patrol Cave": [], "Overworld above Patrol Cave": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]] + [["Hyperdash"], ["LS1"]] }, "Overworld Special Shop Entry": { "East Overworld": @@ -800,8 +830,8 @@ class DeadEnd(IntEnum): "Overworld Belltower": { "Overworld Belltower at Bell": [], - "Overworld": - [], + # "Overworld": + # [], "Overworld to West Garden Upper": [], }, @@ -809,19 +839,19 @@ class DeadEnd(IntEnum): "Overworld Belltower": [], }, - "Overworld Swamp Upper Entry": { - "Overworld": - [], - }, - "Overworld Swamp Lower Entry": { - "Overworld": - [], - }, + # "Overworld Swamp Upper Entry": { + # "Overworld": + # [], + # }, + # "Overworld Swamp Lower Entry": { + # "Overworld": + # [], + # }, "Overworld Beach": { - "Overworld": - [], + # "Overworld": + # [], "Overworld West Garden Laurels Entry": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"]], "Overworld to Atoll Upper": [], "Overworld Tunnel Turret": @@ -832,38 +862,37 @@ class DeadEnd(IntEnum): [["Hyperdash"]], }, "Overworld to Atoll Upper": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Tunnel Turret": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Well Ladder": { - "Overworld": - [], + # "Overworld": + # [], }, "Overworld at Patrol Cave": { "East Overworld": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"], ["IG1"]], "Overworld above Patrol Cave": [], }, "Overworld above Patrol Cave": { - "Overworld": - [], + # "Overworld": + # [], "East Overworld": [], "Upper Overworld": [], "Overworld at Patrol Cave": [], - "Overworld Belltower at Bell": - [["NMG"]], + # readd long dong if we ever do a misc tricks option }, "Upper Overworld": { "Overworld above Patrol Cave": @@ -878,51 +907,49 @@ class DeadEnd(IntEnum): [], }, "Overworld above Quarry Entrance": { - "Overworld": - [], + # "Overworld": + # [], "Upper Overworld": [], }, "Overworld Quarry Entry": { "Overworld after Envoy": [], - "Overworld": - [["NMG"]], + # "Overworld": + # [["IG1"]], }, "Overworld after Envoy": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Quarry Entry": [], }, "After Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "Above Ruined Passage": [], - "East Overworld": - [["NMG"]], }, "Above Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "After Ruined Passage": [], "East Overworld": [], }, - "Overworld Ruined Passage Door": { - "Overworld": - [["Hyperdash", "NMG"]], - }, - "Overworld Town Portal": { - "Overworld": - [], - }, - "Overworld Spawn Portal": { - "Overworld": - [], - }, + # "Overworld Ruined Passage Door": { + # "Overworld": + # [["Hyperdash", "Zip"]], + # }, + # "Overworld Town Portal": { + # "Overworld": + # [], + # }, + # "Overworld Spawn Portal": { + # "Overworld": + # [], + # }, "Cube Cave Entrance Region": { "Overworld": [], @@ -933,7 +960,7 @@ class DeadEnd(IntEnum): }, "Old House Back": { "Old House Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Furnace Fuse": { "Furnace Ladder Area": @@ -941,9 +968,9 @@ class DeadEnd(IntEnum): }, "Furnace Ladder Area": { "Furnace Fuse": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Furnace Walking Path": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Furnace Walking Path": { "Furnace Ladder Area": @@ -971,7 +998,7 @@ class DeadEnd(IntEnum): }, "East Forest": { "East Forest Dance Fox Spot": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], "East Forest Portal": [], "Lower Forest": @@ -979,7 +1006,7 @@ class DeadEnd(IntEnum): }, "East Forest Dance Fox Spot": { "East Forest": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "East Forest Portal": { "East Forest": @@ -995,7 +1022,7 @@ class DeadEnd(IntEnum): }, "Guard House 1 West": { "Guard House 1 East": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Guard House 2 Upper": { "Guard House 2 Lower": @@ -1007,19 +1034,19 @@ class DeadEnd(IntEnum): }, "Forest Grave Path Main": { "Forest Grave Path Upper": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG3"]], "Forest Grave Path by Grave": [], }, "Forest Grave Path Upper": { "Forest Grave Path Main": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "Forest Grave Path by Grave": { "Forest Hero's Grave": [], "Forest Grave Path Main": - [["NMG"]], + [["IG1"]], }, "Forest Hero's Grave": { "Forest Grave Path by Grave": @@ -1051,7 +1078,7 @@ class DeadEnd(IntEnum): }, "Dark Tomb Checkpoint": { "Well Boss": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Dark Tomb Entry Point": { "Dark Tomb Upper": @@ -1075,13 +1102,13 @@ class DeadEnd(IntEnum): }, "West Garden": { "West Garden Laurels Exit Region": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "West Garden after Boss": [], "West Garden Hero's Grave Region": [], "West Garden Portal Item": - [["NMG"]], + [["IG2"]], }, "West Garden Laurels Exit Region": { "West Garden": @@ -1093,13 +1120,19 @@ class DeadEnd(IntEnum): }, "West Garden Portal Item": { "West Garden": - [["NMG"]], - "West Garden Portal": - [["Hyperdash", "West Garden"]], + [["IG1"]], + "West Garden by Portal": + [["Hyperdash"]], }, - "West Garden Portal": { + "West Garden by Portal": { "West Garden Portal Item": [["Hyperdash"]], + "West Garden Portal": + [["West Garden"]], + }, + "West Garden Portal": { + "West Garden by Portal": + [], }, "West Garden Hero's Grave Region": { "West Garden": @@ -1107,7 +1140,7 @@ class DeadEnd(IntEnum): }, "Ruined Atoll": { "Ruined Atoll Lower Entry Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Ruined Atoll Ladder Tops": [], "Ruined Atoll Frog Mouth": @@ -1174,11 +1207,17 @@ class DeadEnd(IntEnum): [], }, "Library Exterior Ladder Region": { + "Library Exterior by Tree": + [], + }, + "Library Exterior by Tree": { "Library Exterior Tree Region": [], + "Library Exterior Ladder Region": + [], }, "Library Exterior Tree Region": { - "Library Exterior Ladder Region": + "Library Exterior by Tree": [], }, "Library Hall Bookshelf": { @@ -1223,15 +1262,21 @@ class DeadEnd(IntEnum): "Library Lab": { "Library Lab Lower": [["Hyperdash"]], - "Library Portal": + "Library Lab on Portal Pad": [], "Library Lab to Librarian": [], }, - "Library Portal": { + "Library Lab on Portal Pad": { + "Library Portal": + [], "Library Lab": [], }, + "Library Portal": { + "Library Lab on Portal Pad": + [], + }, "Library Lab to Librarian": { "Library Lab": [], @@ -1240,11 +1285,9 @@ class DeadEnd(IntEnum): "Fortress Exterior from Overworld": [], "Fortress Courtyard Upper": - [["UR"]], - "Fortress Exterior near cave": - [["UR"]], + [["LS2"]], "Fortress Courtyard": - [["UR"]], + [["LS1"]], }, "Fortress Exterior from Overworld": { "Fortress Exterior from East Forest": @@ -1252,15 +1295,15 @@ class DeadEnd(IntEnum): "Fortress Exterior near cave": [], "Fortress Courtyard": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], }, "Fortress Exterior near cave": { "Fortress Exterior from Overworld": - [["Hyperdash"], ["UR"]], - "Fortress Courtyard": - [["UR"]], + [["Hyperdash"], ["LS1"]], + "Fortress Courtyard": # ice grapple hard: shoot far fire pot, it aggros one of the enemies over to you + [["IG3"], ["LS1"]], "Fortress Courtyard Upper": - [["UR"]], + [["LS2"]], "Beneath the Vault Entry": [], }, @@ -1270,7 +1313,7 @@ class DeadEnd(IntEnum): }, "Fortress Courtyard": { "Fortress Courtyard Upper": - [["NMG"]], + [["IG1"]], "Fortress Exterior from Overworld": [["Hyperdash"]], }, @@ -1296,7 +1339,7 @@ class DeadEnd(IntEnum): }, "Fortress East Shortcut Lower": { "Fortress East Shortcut Upper": - [["NMG"]], + [["IG1"]], }, "Fortress East Shortcut Upper": { "Fortress East Shortcut Lower": @@ -1304,11 +1347,11 @@ class DeadEnd(IntEnum): }, "Eastern Vault Fortress": { "Eastern Vault Fortress Gold Door": - [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], + [["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], }, "Eastern Vault Fortress Gold Door": { "Eastern Vault Fortress": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path": { "Fortress Hero's Grave Region": @@ -1318,7 +1361,7 @@ class DeadEnd(IntEnum): }, "Fortress Grave Path Upper": { "Fortress Grave Path": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path Dusty Entrance Region": { "Fortress Grave Path": @@ -1346,7 +1389,7 @@ class DeadEnd(IntEnum): }, "Monastery Back": { "Monastery Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], "Monastery Hero's Grave Region": [], }, @@ -1363,6 +1406,8 @@ class DeadEnd(IntEnum): [["Quarry Connector"]], "Quarry": [], + "Monastery Rope": + [["LS2"]], }, "Quarry Portal": { "Quarry Entry": @@ -1374,7 +1419,7 @@ class DeadEnd(IntEnum): "Quarry Back": [["Hyperdash"]], "Monastery Rope": - [["UR"]], + [["LS2"]], }, "Quarry Back": { "Quarry": @@ -1392,7 +1437,7 @@ class DeadEnd(IntEnum): "Quarry Monastery Entry": [], "Lower Quarry Zig Door": - [["NMG"]], + [["IG3"]], }, "Lower Quarry": { "Even Lower Quarry": @@ -1402,7 +1447,7 @@ class DeadEnd(IntEnum): "Lower Quarry": [], "Lower Quarry Zig Door": - [["Quarry", "Quarry Connector"], ["NMG"]], + [["Quarry", "Quarry Connector"], ["IG3"]], }, "Monastery Rope": { "Quarry Back": @@ -1430,7 +1475,7 @@ class DeadEnd(IntEnum): }, "Rooted Ziggurat Lower Back": { "Rooted Ziggurat Lower Front": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG1"]], "Rooted Ziggurat Portal Room Entrance": [], }, @@ -1443,26 +1488,35 @@ class DeadEnd(IntEnum): [], }, "Rooted Ziggurat Portal Room Exit": { - "Rooted Ziggurat Portal": + "Rooted Ziggurat Portal Room": [], }, - "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": { + "Rooted Ziggurat Portal": + [], "Rooted Ziggurat Portal Room Exit": [["Rooted Ziggurat Lower Back"]], }, + "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": + [], + }, "Swamp Front": { "Swamp Mid": [], + # get one pillar from the gate, then dash onto the gate, very tricky + "Back of Swamp Laurels Area": + [["Hyperdash", "Zip"]], }, "Swamp Mid": { "Swamp Front": [], "Swamp to Cathedral Main Entrance Region": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG2"], ["LS3"]], "Swamp Ledge under Cathedral Door": [], "Back of Swamp": - [["UR"]], + [["LS1"]], # ig3 later? }, "Swamp Ledge under Cathedral Door": { "Swamp Mid": @@ -1476,24 +1530,41 @@ class DeadEnd(IntEnum): }, "Swamp to Cathedral Main Entrance Region": { "Swamp Mid": - [["NMG"]], + [["IG1"]], }, "Back of Swamp": { "Back of Swamp Laurels Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"]], "Swamp Hero's Grave Region": [], + "Swamp Mid": + [["LS2"]], + "Swamp Front": + [["LS1"]], + "Swamp to Cathedral Main Entrance Region": + [["LS3"]], + "Swamp to Cathedral Treasure Room": + [["LS3"]] }, "Back of Swamp Laurels Area": { "Back of Swamp": [["Hyperdash"]], + # get one pillar from the gate, then dash onto the gate, very tricky "Swamp Mid": - [["NMG", "Hyperdash"]], + [["IG1", "Hyperdash"], ["Hyperdash", "Zip"]], }, "Swamp Hero's Grave Region": { "Back of Swamp": [], }, + "Cathedral": { + "Cathedral to Gauntlet": + [], + }, + "Cathedral to Gauntlet": { + "Cathedral": + [], + }, "Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet": [], diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 3d1973beb375..65175e41ca14 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,8 +1,10 @@ -from typing import Dict, Set, List, Tuple, TYPE_CHECKING +from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item +from .options import IceGrappling, LadderStorage from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, - bomb_walls) -from .er_data import Portal + laurels_zip, bomb_walls) +from .er_data import Portal, get_portal_outlet_region +from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -82,13 +84,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld"]) + # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, world)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], rule=lambda state: has_ladder("Ladders to West Bell", state, world)) @@ -97,32 +102,35 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Belltower at Bell"], 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( - connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: options.logic_rules and state.has(fire_wand, player)) + # long dong, do not make a reverse connection here or to belltower, maybe readd later + # regions["Overworld above Patrol Cave"].connect( + # connecting_region=regions["Overworld Belltower at Bell"], + # rule=lambda state: options.logic_rules and state.has(fire_wand, player)) - # nmg: can laurels through the ruined passage door + # can laurels through the ruined passage door at either corner regions["Overworld"].connect( connecting_region=regions["Overworld Ruined Passage Door"], rule=lambda state: state.has(key, player, 2) - or (state.has(laurels, player) and options.logic_rules)) + or laurels_zip(state, world)) regions["Overworld Ruined Passage Door"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) + # for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down + # you can ice grapple through the bomb wall, so no need for shop logic checking regions["Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or state.has(laurels, player)) + or state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -138,7 +146,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -147,15 +155,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -169,7 +177,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) @@ -185,7 +193,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -193,7 +201,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) @@ -206,13 +214,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Upper Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) + # ice grapple push guard captain down the ledge regions["Upper Overworld"].connect( connecting_region=regions["Overworld after Temple Rafters"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) regions["Overworld after Temple Rafters"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], @@ -224,13 +234,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], @@ -242,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -256,7 +264,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, world)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Swamp Lower Entry"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladder to Swamp", state, world)) @@ -279,20 +288,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - # not including ice grapple through this because it's very tedious to get an enemy here + # lure enemy over and ice grapple through regions["Overworld"].connect( connecting_region=regions["Overworld Southeast Cross Door"], - rule=lambda state: has_ability(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Southeast Cross Door"].connect( connecting_region=regions["Overworld"], 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(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) @@ -312,7 +322,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -325,12 +335,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + rule=lambda state: state.has(laurels, player)) regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) @@ -341,13 +350,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Cube Cave Entrance Region"].connect( connecting_region=regions["Overworld"]) + # drop a rudeling down, icebolt or ice bomb + regions["Overworld"].connect( + connecting_region=regions["Overworld to West Garden from Furnace"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # Overworld side areas regions["Old House Front"].connect( connecting_region=regions["Old House Back"]) - # nmg: laurels through the gate + # laurels through the gate, use left wall to space yourself regions["Old House Back"].connect( connecting_region=regions["Old House Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Sealed Temple"].connect( connecting_region=regions["Sealed Temple Rafters"]) @@ -388,15 +402,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Forest Belltower Lower"], rule=lambda state: has_ladder("Ladder to East Forest", state, world)) - # nmg: ice grapple up to dance fox spot, and vice versa + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], @@ -407,7 +421,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Forest"].connect( connecting_region=regions["Lower Forest"], 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))) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Lower Forest"].connect( connecting_region=regions["East Forest"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) @@ -425,22 +439,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Guard House 2 Upper"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) - # nmg: ice grapple from upper grave path exit to the rest of it + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + # for the ice grapple, lure a rudeling up top, then grapple push it across regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path Upper"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path by Grave"]) - # nmg: ice grapple or laurels through the gate + # 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, world) - or (state.has(laurels, player) and options.logic_rules)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + or laurels_zip(state, world)) regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Hero's Grave"], @@ -473,10 +489,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Well Boss"].connect( connecting_region=regions["Dark Tomb Checkpoint"]) - # nmg: can laurels through the gate + # can laurels through the gate, no setup needed regions["Dark Tomb Checkpoint"].connect( connecting_region=regions["Well Boss"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], @@ -505,12 +521,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) + # you can grapple Garden Knight to aggro it, then ledge it regions["West Garden after Boss"].connect( connecting_region=regions["West Garden"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + # ice grapple push Garden Knight off the side regions["West Garden"].connect( connecting_region=regions["West Garden after Boss"], - rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + rule=lambda state: state.has(laurels, player) or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Hero's Grave Region"], @@ -519,26 +539,32 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden"]) regions["West Garden Portal"].connect( + connecting_region=regions["West Garden by Portal"]) + regions["West Garden by Portal"].connect( + connecting_region=regions["West Garden Portal"], + rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player)) + + regions["West Garden by Portal"].connect( connecting_region=regions["West Garden Portal Item"], 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(prayer, state, world)) + connecting_region=regions["West Garden by Portal"], + rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple to and from the item behind the magic dagger house + # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) # Atoll and Frog's Domain - # nmg: ice grapple the bird below the portal + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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)) @@ -570,13 +596,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], rule=lambda state: has_ability(prayer, state, world) - and has_ladder("Ladders in South Atoll", state, world)) + and (has_ladder("Ladders in South Atoll", state, world) + # shoot fuse and have the shot hit you mid-LS + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard))) 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, world)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) @@ -605,14 +635,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # Library regions["Library Exterior Tree Region"].connect( + connecting_region=regions["Library Exterior by Tree"]) + regions["Library Exterior by Tree"].connect( + connecting_region=regions["Library Exterior Tree Region"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Library Exterior by Tree"].connect( connecting_region=regions["Library Exterior Ladder Region"], rule=lambda state: state.has_any({grapple, laurels}, player) 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(prayer, state, world) - and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world)) - or state.has(grapple, player))) + connecting_region=regions["Library Exterior by Tree"], + rule=lambda state: state.has(grapple, player) + or (state.has(laurels, player) and has_ladder("Ladders in Library", state, world))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], @@ -658,14 +693,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( - connecting_region=regions["Library Portal"], - 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 on Portal Pad"], + rule=lambda state: has_ladder("Ladders in Library", state, world)) + regions["Library Lab on Portal Pad"].connect( connecting_region=regions["Library Lab"], rule=lambda state: has_ladder("Ladders in Library", state, world) or state.has(laurels, player)) + regions["Library Lab on Portal Pad"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(prayer, state, world)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab on Portal Pad"]) + regions["Library Lab"].connect( connecting_region=regions["Library Lab to Librarian"], rule=lambda state: has_ladder("Ladders in Library", state, world)) @@ -688,6 +728,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Fortress Exterior near cave"], rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) + # shoot far fire pot, enemy gets aggro'd + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Courtyard"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, 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, world)) @@ -702,14 +747,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) @@ -733,17 +778,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], @@ -761,7 +806,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -819,25 +864,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world) and options.entrance_rando)) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) # 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, world) and options.entrance_rando) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) - # nmg: can laurels through the gate + # laurels through the gate, no setup needed regions["Monastery Back"].connect( connecting_region=regions["Monastery Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Monastery Back"].connect( connecting_region=regions["Monastery Hero's Grave Region"], @@ -863,14 +908,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) 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, world)) - and has_ability(prayer, state, world) - and has_sword(state, player)) - or can_ladder_storage(state, world)) + rule=lambda state: (state.has(laurels, player) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + and has_ability(prayer, state, world) + and has_sword(state, player)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], @@ -882,40 +926,62 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Front"]) regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room"]) + regions["Rooted Ziggurat Portal Room"].connect( + connecting_region=regions["Rooted Ziggurat Portal"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Rooted Ziggurat Portal Room"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], 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(prayer, state, world)) + connecting_region=regions["Rooted Ziggurat Portal Room"]) # Swamp and Cathedral regions["Swamp Front"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Swamp Mid"].connect( connecting_region=regions["Swamp Front"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # nmg: ice grapple through cathedral door, can do it both ways - regions["Swamp Mid"].connect( + # a whole lot of stuff to basically say "you need to pray at the overworld fuse" + swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, world)) + rule=lambda state: (has_ability(prayer, state, world) + and (state.has(laurels, player) + # blam yourself in the face with a wand shot off the fuse + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard + and (not options.shuffle_ladders + or state.has_any({"Ladders in Overworld Town", + "Ladder to Swamp", + "Ladders near Weathervane"}, player) + or (state.has("Ladder to Ruined Atoll", player) + and state.can_reach_region("Overworld Beach", player)))))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + + if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: + world.multiworld.register_indirect_condition(regions["Overworld Beach"], swamp_mid_to_cath) + regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + # grapple push the enemy by the door down, then grapple to it. Really jank regions["Swamp Mid"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"], - rule=lambda state: has_ladder("Ladders in Swamp", state, world)) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # ice grapple enemy standing at the door regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp Mid"], 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 + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], @@ -930,11 +996,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Back of Swamp"], rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple down while you're on the pillars + # ice grapple down from the pillar, or do that really annoying laurels zip + # the zip goes to front or mid, just doing mid since mid -> front can be done with laurels alone 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, world)) + rule=lambda state: laurels_zip(state, world) + or (state.has(laurels, player) + and has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))) + # get one pillar from the gate, then dash onto the gate, very tricky + regions["Swamp Front"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: laurels_zip(state, world)) regions["Back of Swamp"].connect( connecting_region=regions["Swamp Hero's Grave Region"], @@ -942,6 +1014,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) + regions["Cathedral"].connect( + connecting_region=regions["Cathedral to Gauntlet"], + rule=lambda state: (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + or options.entrance_rando) # elevator is always there in ER + regions["Cathedral to Gauntlet"].connect( + connecting_region=regions["Cathedral"]) + regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1000,337 +1080,141 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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 - if options.logic_rules == "unrestricted": + if options.ladder_storage: def get_portal_info(portal_sd: str) -> Tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: - return portal1.name, portal2.region + return portal1.name, get_portal_outlet_region(portal2, world) if portal2.scene_destination() == portal_sd: - return portal2.name, portal1.region + return portal2.name, get_portal_outlet_region(portal1, world) raise Exception("no matches found in get_paired_region") - ladder_storages: List[Tuple[str, str, Set[str]]] = [ - # LS from Overworld main - # The upper Swamp entrance - ("Overworld", "Overworld Redux, Swamp Redux 2_wall", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper atoll entrance - ("Overworld", "Overworld Redux, Atoll Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld", "Overworld Redux, Furnace_gyro_west", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper West Garden entry, by the belltower - ("Overworld", "Overworld Redux, Archipelagos Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Ruined Passage - ("Overworld", "Overworld Redux, Ruins Passage_east", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Quarry entry - ("Overworld", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}), - # East Forest entry - ("Overworld", "Overworld Redux, Forest Belltower_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Fortress entry - ("Overworld", "Overworld Redux, Fortress Courtyard_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Patrol Cave entry - ("Overworld", "Overworld Redux, PatrolCave_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld", "Overworld Redux, ShopSpecial_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential - ("Overworld", "Overworld Redux, Temple_rafters", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Spot above the Quarry entrance, - # only gets you to the mountain stairs - ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_", - {"Ladders near Dark Tomb"}), - - # LS from the Overworld Beach - # West Garden entry by the Furnace - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # West Garden laurels entry - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp lower entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Rotating Lights entrance - ("Overworld Beach", "Overworld Redux, Overworld Cave_", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp upper entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall", - {"Ladder to Ruined Atoll"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld Beach", "Overworld Redux, Furnace_gyro_west", - {"Ladder to Ruined Atoll"}), - # Upper West Garden entry, by the belltower - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper", - {"Ladder to Ruined Atoll"}), - # Ruined Passage - ("Overworld Beach", "Overworld Redux, Ruins Passage_east", - {"Ladder to Ruined Atoll"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct", - {"Ladder to Ruined Atoll"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladder to Ruined Atoll"}), - # Quarry entry - ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_", - {"Ladder to Ruined Atoll"}), - - # LS from that low spot where you normally walk to swamp - # Only has low ones you can't get to from main Overworld - # West Garden main entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower", - {"Ladder to Swamp"}), - # Maze Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_", - {"Ladder to Swamp"}), - # Hourglass Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach", - {"Ladder to Swamp"}), - # Lower Atoll entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower", - {"Ladder to Swamp"}), - # Lowest West Garden entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladder to Swamp"}), - - # from the ladders by the belltower - # Ruined Passage - ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east", - {"Ladders to West Bell"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders to West Bell"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders to West Bell"}), - # Quarry entry - ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders to West Bell"}), - # East Forest entry - ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_", - {"Ladders to West Bell"}), - # Fortress entry - ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_", - {"Ladders to West Bell"}), - # Patrol Cave entry - ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_", - {"Ladders to West Bell"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_", - {"Ladders to West Bell"}), - # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters", - {"Ladders to West Bell"}), - - # In the furnace - # Furnace ladder to the fuse entrance - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()), - # Furnace ladder to Dark Tomb - ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()), - # Furnace ladder to the West Garden connector - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()), - - # West Garden - # exit after Garden Knight - ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()), - # West Garden laurels exit - ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()), - - # Atoll, use the little ladder you fix at the beginning - ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()), - - # East Forest - # Entrance by the dancing fox holy cross spot - ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()), - - # From the west side of Guard House 1 to the east side - ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()), - ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()), - - # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch - ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()), - - # Fortress Exterior - # shop, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()), - # Fortress main entry and grave path lower entry, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the east side of the area - ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the Beneath the Vault entrance ladder - ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", - {"Ladder to Beneath the Vault"}), - - # ls at the ladder, need to gain a little height to get up the stairs - # excluded in non-ER due to soft lock potential - ("Lower Mountain", "Mountain, Mountaintop_", set()), - - # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword - ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()), - - # Swamp to Gauntlet - ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", - {"Ladders in Swamp"}), - # Swamp to Overworld upper - ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", - {"Ladders in Swamp"}), - # Ladder by the hero grave - ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()), - ("Back of Swamp", "Swamp Redux 2, Shop_", set()), - # Need to put the cathedral HC code mid-flight - ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()), - ] - - for region_name, scene_dest, ladders in ladder_storages: - portal_name, paired_region = get_portal_info(scene_dest) - # this is the only exception, requiring holy cross as well - if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp": - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - 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: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and (state.has("Ladders to West Bell", player))) - # soft locked unless you have either ladder. if you have laurels, you use the other Entrance - elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \ - 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({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) - # soft locked for the same reasons as above - elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \ - 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_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: - 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_any({"Ladders to West Bell", laurels}, player)) - # soft locked if you can't get back out - elif portal_name == "Fortress Courtyard to Beneath the Vault" 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("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) - and (state.has_any({"Ladders in Overworld Town", grapple}, player) - 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(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: + # connect ls elevation regions to their destinations + def ls_connect(origin_name: str, portal_sdt: str) -> None: + p_name, paired_region_name = get_portal_info(portal_sdt) + ladder_regions[origin_name].connect( + regions[paired_region_name], + name=p_name + " (LS) " + origin_name) + + # get what non-overworld ladder storage connections we want together + non_ow_ls_list = [] + non_ow_ls_list.extend(easy_ls) + if options.ladder_storage >= LadderStorage.option_medium: + non_ow_ls_list.extend(medium_ls) + if options.ladder_storage >= LadderStorage.option_hard: + non_ow_ls_list.extend(hard_ls) + + # create the ls elevation regions + ladder_regions: Dict[str, Region] = {} + for name in ow_ladder_groups.keys(): + ladder_regions[name] = Region(name, player, world.multiworld) + + # connect the ls elevations to each other where applicable + if options.ladder_storage >= LadderStorage.option_medium: + for i in range(len(ow_ladder_groups) - 1): + ladder_regions[f"LS Elev {i}"].connect(ladder_regions[f"LS Elev {i + 1}"]) + + # connect the applicable overworld regions to the ls elevation regions + for origin_region, ladders in region_ladders.items(): + for ladder_region, region_info in ow_ladder_groups.items(): + # checking if that region has a ladder or ladders for that elevation + common_ladders: FrozenSet[str] = frozenset(ladders.intersection(region_info.ladders)) + if common_ladders: + if options.shuffle_ladders: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state, lads=common_ladders: state.has_any(lads, player) + and can_ladder_storage(state, world)) + else: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state: can_ladder_storage(state, world)) + + # connect ls elevation regions to the region on the other side of the portals + for ladder_region, region_info in ow_ladder_groups.items(): + for portal_dest in region_info.portals: + ls_connect(ladder_region, "Overworld Redux, " + portal_dest) + + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail + if options.ladder_storage >= LadderStorage.option_medium: + for ladder_region, region_info in ow_ladder_groups.items(): + for dest_region in region_info.regions: + ladder_regions[ladder_region].connect( + connecting_region=regions[dest_region], + name=ladder_region + " (LS) " + dest_region) + # well rail, need height off portal pad for one side, and a tiny extra from stairs on the other + ls_connect("LS Elev 3", "Overworld Redux, Sewer_west_aqueduct") + ls_connect("LS Elev 3", "Overworld Redux, Furnace_gyro_upper_north") + + # connect ls elevation regions to portals where you need to get behind the map to enter it + if options.ladder_storage >= LadderStorage.option_hard: + ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") + ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") + ls_connect("LS Elev 5", "Overworld Redux, Temple_main") + + # connect the non-overworld ones + for ls_info in non_ow_ls_list: + # for places where the destination is a region (so you have to get knocked down) + if ls_info.dest_is_region: + # none of the non-ow ones have multiple ladders that can be used, so don't need has_any + if options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) continue - # soft lock if you don't have the ladder, I regret writing unrestricted logic - elif portal_name == "Temple Rafters 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("Ladder near Temple Rafters", player) - or (state.has_all({laurels, grapple}, player) - and ((state.has("Ladders near Patrol Cave", player) - and (state.has("Ladders near Dark Tomb", player) - 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, 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( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player)) - # one ladder required - elif len(ladders) == 1: - ladder = ladders.pop() - 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)) - # if multiple ladders can be used + + portal_name, dest_region = get_portal_info(ls_info.destination) + # these two are special cases + if ls_info.destination == "Atoll Redux, Frog Stairs_mouth": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) + and (has_ladder("Ladders in South Atoll", state, world) + or state.has(key, player, 2) # can do it from the rope + # ice grapple push a crab into the door + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or options.ladder_storage >= LadderStorage.option_medium)) # use the little ladder + # holy cross mid-ls to get in here + elif ls_info.destination == "Swamp Redux 2, Cathedral Redux_secret": + if ls_info.origin == "Swamp Mid": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world) + and has_ladder("Ladders in Swamp", state, world)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world)) + + elif options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) 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)) + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) + + for region in ladder_regions.values(): + world.multiworld.regions.append(region) def set_er_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -1439,10 +1323,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -1465,23 +1352,25 @@ def set_er_location_rules(world: "TunicWorld") -> None: 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 + # if ER is off, while you can get the chest, you won't be able to actually get through zig set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), - lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) - or options.entrance_rando))) + lambda state: has_sword(state, player) or (state.has(fire_wand, player) + and (state.has(laurels, player) + or world.options.entrance_rando))) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), lambda state: has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player)) - # nmg - kill Librarian with a lure, or gun I guess set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: (has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player) and has_ladder("Ladders in Library", state, world)) - # nmg - kill boss scav with orb + firecracker, or similar + # can ice grapple boss scav off the side + # the grapple from the other side of the bridge isn't in logic 'cause we don't have a misc tricks option set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), @@ -1490,7 +1379,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - # these two swamp checks really want you to kill the big skeleton first + # really hard to do 4 skulls with a big skeleton chasing you around set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) @@ -1541,7 +1430,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or can_shop(state, world)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or can_shop(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # not enough space to ice grapple into here + set_rule(world.get_location("Quarry - [East] Bombable Wall"), + lambda state: state.has(gun, player) or can_shop(state, world)) # Shop set_rule(world.get_location("Shop - Potion 1"), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index e7c8fd58d0c6..05f6177aa57d 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,7 @@ -from typing import Dict, List, Set, TYPE_CHECKING +from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd +from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo from .er_rules import set_er_region_rules from Options import PlandoConnection from .options import EntranceRando @@ -22,17 +22,18 @@ class TunicERLocation(Location): def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} + for region_name, region_data in world.er_regions.items(): + regions[region_name] = Region(region_name, world.player, world.multiworld) + if world.options.entrance_rando: - portal_pairs = pair_portals(world) + portal_pairs = pair_portals(world, regions) + # output the entrances to the spoiler log here for convenience sorted_portal_pairs = sort_portals(portal_pairs) for portal1, portal2 in sorted_portal_pairs.items(): world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) else: - portal_pairs = vanilla_portals() - - for region_name, region_data in tunic_er_regions.items(): - regions[region_name] = Region(region_name, world.player, world.multiworld) + portal_pairs = vanilla_portals(world, regions) set_er_region_rules(world, regions, portal_pairs) @@ -93,7 +94,18 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: region.locations.append(location) -def vanilla_portals() -> Dict[Portal, Portal]: +# all shops are the same shop. however, you cannot get to all shops from the same shop entrance. +# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back +def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None: + new_shop_name = f"Shop {world.shop_num}" + world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats) + new_shop_region = Region(new_shop_name, world.player, world.multiworld) + new_shop_region.connect(regions["Shop"]) + regions[new_shop_name] = new_shop_region + world.shop_num += 1 + + +def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"] @@ -105,8 +117,9 @@ def vanilla_portals() -> Dict[Portal, Portal]: portal2_sdt = portal1.destination_scene() if portal2_sdt.startswith("Shop,"): - portal2 = Portal(name="Shop", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) elif portal2_sdt == "Purgatory, Purgatory_bottom": portal2_sdt = "Purgatory, Purgatory_top" @@ -125,14 +138,15 @@ def vanilla_portals() -> Dict[Portal, Portal]: # pairing off portals, starting with dead ends -def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: - # separate the portals into dead ends and non-dead ends +def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} dead_ends: List[Portal] = [] two_plus: List[Portal] = [] player_name = world.player_name portal_map = portal_mapping.copy() - logic_rules = world.options.logic_rules.value + laurels_zips = world.options.laurels_zips.value + ice_grappling = world.options.ice_grappling.value + ladder_storage = world.options.ladder_storage.value fixed_shop = world.options.fixed_shop laurels_location = world.options.laurels_location traversal_reqs = deepcopy(traversal_requirements) @@ -142,19 +156,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # if it's not one of the EntranceRando options, it's a custom seed if world.options.entrance_rando.value not in EntranceRando.options.values(): seed_group = world.seed_groups[world.options.entrance_rando.value] - logic_rules = seed_group["logic_rules"] + laurels_zips = seed_group["laurels_zips"] + ice_grappling = seed_group["ice_grappling"] + ladder_storage = seed_group["ladder_storage"] fixed_shop = seed_group["fixed_shop"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False + logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) + # marking that you don't immediately have laurels if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"): has_laurels = False - shop_scenes: Set[str] = set() shop_count = 6 if fixed_shop: shop_count = 0 - shop_scenes.add("Overworld Redux") else: # if fixed shop is off, remove this portal for portal in portal_map: @@ -169,13 +185,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # create separate lists for dead ends and non-dead ends for portal in portal_map: - dead_end_status = tunic_er_regions[portal.region].dead_end + dead_end_status = world.er_regions[portal.region].dead_end if dead_end_status == DeadEnd.free: two_plus.append(portal) elif dead_end_status == DeadEnd.all_cats: dead_ends.append(portal) elif dead_end_status == DeadEnd.restricted: - if logic_rules: + if ice_grappling: two_plus.append(portal) else: dead_ends.append(portal) @@ -196,7 +212,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # make better start region stuff when/if implementing random start start_region = "Overworld" connected_regions.add(start_region) - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value @@ -225,12 +241,14 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) non_dead_end_regions = set() - for region_name, region_info in tunic_er_regions.items(): + for region_name, region_info in world.er_regions.items(): if not region_info.dead_end: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 2 and logic_rules: + # if ice grappling to places is in logic, both places stop being dead ends + elif region_info.dead_end == DeadEnd.restricted and ice_grappling: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 3: + # secret gathering place and zig skip get weird, special handling + elif region_info.dead_end == DeadEnd.special: if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \ or (region_name == "Zig Skip Exit" and fixed_shop): non_dead_end_regions.add(region_name) @@ -239,6 +257,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: for connection in plando_connections: p_entrance = connection.entrance p_exit = connection.exit + # if you plando secret gathering place, need to know that during portal pairing + if "Secret Gathering Place Exit" in [p_entrance, p_exit]: + waterfall_plando = True portal1_dead_end = True portal2_dead_end = True @@ -285,16 +306,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: break # if it's not a dead end, it might be a shop if p_exit == "Shop Portal": - portal2 = Portal(name="Shop Portal", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) shop_count -= 1 # need to maintain an even number of portals total if shop_count < 0: shop_count += 2 - for p in portal_mapping: - if p.name == p_entrance: - shop_scenes.add(p.scene()) - break # and if it's neither shop nor dead end, it just isn't correct else: if not portal2: @@ -327,11 +345,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: raise Exception(f"{player_name} paired a dead end to a dead end in their " "plando connections.") - waterfall_plando = True portal_pairs[portal1] = portal2 # if we have plando connections, our connected regions may change somewhat - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): portal1 = None @@ -343,7 +360,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: raise Exception(f"Failed to do Fixed Shop option. " f"Did {player_name} plando connection the Windmill Shop entrance?") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 two_plus.remove(portal1) @@ -393,7 +412,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: if waterfall_plando: cr = connected_regions.copy() cr.add(portal.region) - if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules): + if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks): continue elif portal.region != "Secret Gathering Place": continue @@ -405,9 +424,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # once we have both portals, connect them and add the new region(s) to connected_regions if check_success == 2: - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if "Secret Gathering Place" in connected_regions: has_laurels = True + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) portal_pairs[portal1] = portal2 check_success = 0 random_object.shuffle(two_plus) @@ -418,16 +437,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: shop_count = 0 for i in range(shop_count): - portal1 = None - for portal in two_plus: - if portal.scene() not in shop_scenes: - shop_scenes.add(portal.scene()) - portal1 = portal - two_plus.remove(portal) - break + portal1 = two_plus.pop() if portal1 is None: - raise Exception("Too many shops in the pool, or something else went wrong.") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + raise Exception("TUNIC: Too many shops in the pool, or something else went wrong.") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 @@ -460,13 +475,12 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic region1 = regions[portal1.region] region2 = regions[portal2.region] region1.connect(connecting_region=region2, name=portal1.name) - # prevent the logic from thinking you can get to any shop-connected region from the shop - if portal2.name not in {"Shop", "Shop Portal"}: - region2.connect(connecting_region=region1, name=portal2.name) + region2.connect(connecting_region=region1, name=portal2.name) def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]], - has_laurels: bool, logic: int) -> Set[str]: + has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]: + zips, ice_grapples, ls = logic # starting count, so we can run it again if this changes region_count = len(connected_regions) for origin, destinations in traversal_reqs.items(): @@ -485,11 +499,15 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s if req == "Hyperdash": if not has_laurels: break - elif req == "NMG": - if not logic: + elif req == "Zip": + if not zips: + break + # if req is higher than logic option, then it breaks since it's not a valid connection + elif req.startswith("IG"): + if int(req[-1]) > ice_grapples: break - elif req == "UR": - if logic < 2: + elif req.startswith("LS"): + if int(req[-1]) > ls: break elif req not in connected_regions: break diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index e0ee17831a0a..55aa3468fc6b 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -166,6 +166,7 @@ class TunicItemData(NamedTuple): "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), } +# items to be replaced by fool traps fool_tiers: List[List[str]] = [ [], ["Money x1", "Money x10", "Money x15", "Money x16"], @@ -173,6 +174,7 @@ class TunicItemData(NamedTuple): ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], ] +# items we'll want the location of in slot data, for generating in-game hints slot_data_item_names = [ "Stick", "Sword", diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py new file mode 100644 index 000000000000..a29d50b4f455 --- /dev/null +++ b/worlds/tunic/ladder_storage_data.py @@ -0,0 +1,186 @@ +from typing import Dict, List, Set, NamedTuple, Optional + + +# ladders in overworld, since it is the most complex area for ladder storage +class OWLadderInfo(NamedTuple): + ladders: Set[str] # ladders where the top or bottom is at the same elevation + portals: List[str] # portals at the same elevation, only those without doors + regions: List[str] # regions where a melee enemy can hit you out of ladder storage + + +# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets +ow_ladder_groups: Dict[str, OWLadderInfo] = { + # lowest elevation + "LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"}, + ["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_", + "Town Basement_beach", "Archipelagos Redux_lower", "Archipelagos Redux_lowest"], + ["Overworld Beach"]), + # also the east filigree room + "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, + ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Overworld Tunnel Turret"]), + # also the fountain filigree room and ruined passage door + "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, + ["Archipelagos Redux_upper", "Ruins Passage_east"], + ["After Ruined Passage"]), + # also old house door + "LS Elev 3": OWLadderInfo({"Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell", + "Ladders in Overworld Town"}, + [], + ["Overworld after Envoy", "East Overworld"]), + # skip top of top ladder next to weathervane level, does not provide logical access to anything + "LS Elev 4": OWLadderInfo({"Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Well", + "Ladders in Overworld Town"}, + ["Darkwoods Tunnel_"], + []), + "LS Elev 5": OWLadderInfo({"Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"}, + ["PatrolCave_", "Forest Belltower_", "Fortress Courtyard_", "ShopSpecial_"], + ["East Overworld"]), + # skip top of belltower, middle of dark tomb ladders, and top of checkpoint, does not grant access to anything + "LS Elev 6": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters"}, + ["Temple_rafters"], + ["Overworld above Patrol Cave"]), + # in-line with the chest above dark tomb, gets you up the mountain stairs + "LS Elev 7": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"}, + ["Mountain_"], + ["Upper Overworld"]), +} + + +# ladders accessible within different regions of overworld, only those that are relevant +# other scenes will just have them hardcoded since this type of structure is not necessary there +region_ladders: Dict[str, Set[str]] = { + "Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", + "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"}, + "Overworld Beach": {"Ladder to Ruined Atoll"}, + "Overworld at Patrol Cave": {"Ladders near Patrol Cave"}, + "Overworld Quarry Entry": {"Ladder to Quarry"}, + "Overworld Belltower": {"Ladders to West Bell"}, + "Overworld after Temple Rafters": {"Ladders near Temple Rafters"}, +} + + +class LadderInfo(NamedTuple): + origin: str # origin region + destination: str # destination portal + ladders_req: Optional[str] = None # ladders required to do this + dest_is_region: bool = False # whether it is a region that you are going to + + +easy_ls: List[LadderInfo] = [ + # In the furnace + # Furnace ladder to the fuse entrance + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"), + # Furnace ladder to Dark Tomb + LadderInfo("Furnace Ladder Area", "Furnace, Crypt Redux_"), + # Furnace ladder to the West Garden connector + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west"), + + # West Garden + # exit after Garden Knight + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"), + # West Garden laurels exit + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"), + + # Atoll, use the little ladder you fix at the beginning + LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"), + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth"), # special case + + # East Forest + # Entrance by the dancing fox holy cross spot + LadderInfo("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper"), + + # From the west side of Guard House 1 to the east side + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate"), + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_"), + + # Fortress Exterior + # shop, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_"), + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower"), + # Use the top of the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_"), + + # same as above, except from the east side of the area + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower"), + + # same as above, except from the Beneath the Vault entrance ladder + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", + "Ladder to Beneath the Vault"), + + # Swamp to Gauntlet + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", "Ladders in Swamp"), + + # Ladder by the hero grave + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Shop_"), +] + +# if we can gain elevation or get knocked down, add the harder ones +medium_ls: List[LadderInfo] = [ + # region-destination versions of easy ls spots + LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True), + # fortress courtyard knockdowns are never logically relevant, the fuse requires upper + LadderInfo("Back of Swamp", "Swamp Mid", dest_is_region=True), + LadderInfo("Back of Swamp", "Swamp Front", dest_is_region=True), + + # gain height off the northeast fuse ramp + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_eye"), + + # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch + LadderInfo("Forest Grave Path Main", "Sword Access, East Forest Redux_upper"), + + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard Upper", "Ladder to Beneath the Vault", + dest_is_region=True), + + # need to gain height to get up the stairs + LadderInfo("Lower Mountain", "Mountain, Mountaintop_"), + + # Where the rope is behind Monastery + LadderInfo("Quarry Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Monastery Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"), + + LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"), + LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True), + + # Swamp to Overworld upper + LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"), +] + +hard_ls: List[LadderInfo] = [ + # lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall + LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True), + # go through the hexagon engraving above the vault door + LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"), + # the turret at the end here is not affected by enemy rando + LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True), + # todo: see if we can use that new laurels strat here + # LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"), + # go behind the cathedral to reach the door, pretty easily doable + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_main", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_main"), + # need to do hc midair, probably cannot get into this without hc + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_secret", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret"), +] diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 09916228163d..442e0c01446d 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -47,7 +47,7 @@ class TunicLocationData(NamedTuple): "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"), "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"), - "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "Lower Forest"), "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), @@ -205,7 +205,7 @@ class TunicLocationData(NamedTuple): "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"), "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"), "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"), - "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Monastery - Monastery Chest": TunicLocationData("Monastery", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), @@ -224,7 +224,7 @@ class TunicLocationData(NamedTuple): "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 92cbafba233f..1683b3ca5aee 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict, Any from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, - PerGameCommonOptions, OptionGroup) + PerGameCommonOptions, OptionGroup, Visibility) from .er_data import portal_mapping @@ -39,27 +39,6 @@ class AbilityShuffling(Toggle): display_name = "Shuffle Abilities" -class LogicRules(Choice): - """ - Set which logic rules to use for your world. - Restricted: Standard logic, no glitches. - No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. - * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer. - Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. - * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. - * Using Ladder Storage to get to individual chests is not in logic to avoid tedium. - * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. - """ - internal_name = "logic_rules" - display_name = "Logic Rules" - option_restricted = 0 - option_no_major_glitches = 1 - alias_nmg = 1 - option_unrestricted = 2 - alias_ur = 2 - default = 0 - - class Lanternless(Toggle): """ Choose whether you require the Lantern for dark areas. @@ -173,8 +152,8 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" - - + + class TunicPlandoConnections(PlandoConnections): """ Generic connection plando. Format is: @@ -189,6 +168,82 @@ class TunicPlandoConnections(PlandoConnections): duplicate_exits = True +class LaurelsZips(Toggle): + """ + Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots. + Notable inclusions are the Monastery gate, Ruined Passage door, Old House gate, Forest Grave Path gate, and getting from the Back of Swamp to the Middle of Swamp. + """ + internal_name = "laurels_zips" + display_name = "Laurels Zips Logic" + + +class IceGrappling(Choice): + """ + Choose whether grappling frozen enemies is in logic. + Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain. + Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. + Hard includes luring or grappling enemies to get to where you want to go. + The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off. + """ + internal_name = "ice_grappling" + display_name = "Ice Grapple Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorage(Choice): + """ + Choose whether Ladder Storage is in logic. + Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.). + Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS. + Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS. + Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Opening individual chests while doing ladder storage is excluded due to tedium. + Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic. + """ + internal_name = "ladder_storage" + display_name = "Ladder Storage Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorageWithoutItems(Toggle): + """ + If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage. + If enabled, you will be expected to perform Ladder Storage without progression items. + This can be done with the plushie code, a Golden Coin, Prayer, and many other options. + + This option has no effect if you do not have Ladder Storage Logic enabled. + """ + internal_name = "ladder_storage_without_items" + display_name = "Ladder Storage without Items" + + +class LogicRules(Choice): + """ + This option has been superseded by the individual trick options. + If set to nmg, it will set Ice Grappling to medium and Laurels Zips on. + If set to ur, it will do nmg as well as set Ladder Storage to medium. + It is here to avoid breaking old yamls, and will be removed at a later date. + """ + visibility = Visibility.none + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + alias_nmg = 1 + option_unrestricted = 2 + alias_ur = 2 + default = 0 + + @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool @@ -199,22 +254,30 @@ class TunicOptions(PerGameCommonOptions): shuffle_ladders: ShuffleLadders entrance_rando: EntranceRando fixed_shop: FixedShop - logic_rules: LogicRules fool_traps: FoolTraps hexagon_quest: HexagonQuest hexagon_goal: HexagonGoal extra_hexagon_percentage: ExtraHexagonPercentage + laurels_location: LaurelsLocation lanternless: Lanternless maskless: Maskless - laurels_location: LaurelsLocation + laurels_zips: LaurelsZips + ice_grappling: IceGrappling + ladder_storage: LadderStorage + ladder_storage_without_items: LadderStorageWithoutItems plando_connections: TunicPlandoConnections + + logic_rules: LogicRules tunic_option_groups = [ OptionGroup("Logic Options", [ - LogicRules, Lanternless, Maskless, + LaurelsZips, + IceGrappling, + LadderStorage, + LadderStorageWithoutItems ]) ] @@ -231,9 +294,12 @@ class TunicOptions(PerGameCommonOptions): "Glace Mode": { "accessibility": "minimal", "ability_shuffling": True, - "entrance_rando": "yes", + "entrance_rando": True, "fool_traps": "onslaught", - "logic_rules": "unrestricted", + "laurels_zips": True, + "ice_grappling": "hard", + "ladder_storage": "hard", + "ladder_storage_without_items": True, "maskless": True, "lanternless": True, }, diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index c30a44bb8ff6..93ec5640e0c2 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -16,7 +16,8 @@ "Eastern Vault Fortress": {"Beneath the Vault"}, "Beneath the Vault": {"Eastern Vault Fortress"}, "Quarry Back": {"Quarry"}, - "Quarry": {"Lower Quarry"}, + "Quarry": {"Monastery", "Lower Quarry"}, + "Monastery": set(), "Lower Quarry": {"Rooted Ziggurat"}, "Rooted Ziggurat": set(), "Swamp": {"Cathedral"}, diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 68756869038d..942bbc773aa5 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -3,7 +3,7 @@ from worlds.generic.Rules import set_rule, forbid_item, add_rule from BaseClasses import CollectionState -from .options import TunicOptions +from .options import TunicOptions, LadderStorage, IceGrappling if TYPE_CHECKING: from . import TunicWorld @@ -27,10 +27,10 @@ blue_hexagon = "Blue Questagon" gold_hexagon = "Gold Questagon" +# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", - "Quarry - [West] Upper Area Bombable Wall", "Quarry - [East] Bombable Wall", - "Ruined Atoll - [Northwest] Bombable Wall"] + "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"] def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: @@ -64,32 +64,33 @@ 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, world: "TunicWorld") -> bool: - player = world.player - if not world.options.logic_rules: +def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.laurels_zips and state.has(laurels, world.player) + + +def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool: + if world.options.ice_grappling < difficulty: return False if not long_range: - return state.has_all({ice_dagger, grapple}, player) + return state.has_all({ice_dagger, grapple}, world.player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) + return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) + if not world.options.ladder_storage: + return False + if world.options.ladder_storage_without_items: + return True + return has_stick(state, world.player) or state.has(grapple, world.player) def has_mask(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.maskless: - return True - else: - return state.has(mask, world.player) + return world.options.maskless or state.has(mask, world.player) def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.lanternless: - return True - else: - return state.has(lantern, world.player) + return world.options.lanternless or state.has(lantern, world.player) def set_region_rules(world: "TunicWorld") -> None: @@ -102,12 +103,14 @@ def set_region_rules(world: "TunicWorld") -> None: lambda state: has_stick(state, player) or state.has(fire_wand, player) world.get_entrance("Overworld -> Dark Tomb").access_rule = \ lambda state: has_lantern(state, world) + # laurels in, ladder storage in through the furnace, or ice grapple down the belltower world.get_entrance("Overworld -> West Garden").access_rule = \ - lambda state: state.has(laurels, player) \ - or can_ladder_storage(state, world) + lambda state: (state.has(laurels, player) + or can_ladder_storage(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, world) \ + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \ or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ @@ -124,8 +127,8 @@ def set_region_rules(world: "TunicWorld") -> None: world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) world.get_entrance("Swamp -> Cathedral").access_rule = \ - lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ - or has_ice_grapple_logic(False, state, world) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \ + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) world.get_entrance("Overworld -> Spirit Arena").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) @@ -133,10 +136,18 @@ def set_region_rules(world: "TunicWorld") -> None: and has_ability(prayer, state, world) and has_sword(state, player) and state.has_any({lantern, laurels}, player)) + world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"), + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world) + and has_ability(prayer, state, world)) + + if options.ladder_storage >= LadderStorage.option_medium: + # ls at any ladder in a safe spot in quarry to get to the monastery rope entrance + world.get_region("Quarry Back").connect(world.get_region("Monastery"), + rule=lambda state: can_ladder_storage(state, world)) + def set_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -147,11 +158,13 @@ def set_location_rules(world: "TunicWorld") -> None: 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))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), 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))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), @@ -186,17 +199,17 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Old House - Normal Chest"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Old House - Holy Cross Chest"), 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))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world))) set_rule(world.get_location("Old House - Shield Pickup"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Overworld - [Southwest] From West Garden"), @@ -206,7 +219,7 @@ def set_location_rules(world: "TunicWorld") -> None: or (has_lantern(state, world) and has_sword(state, player)) or can_ladder_storage(state, world)) set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), - lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + lambda state: state.has_any({grapple, laurels}, player)) set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) set_rule(world.get_location("Special Shop - Secret Page Pickup"), @@ -215,11 +228,11 @@ def set_location_rules(world: "TunicWorld") -> None: 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))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: 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)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("West Furnace - Lantern Pickup"), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -254,7 +267,7 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), @@ -265,12 +278,15 @@ def set_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -285,10 +301,12 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), @@ -301,14 +319,14 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), 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(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) + and (state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), @@ -335,8 +353,16 @@ def set_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: # has_sword is there because you can buy bombs in the shop - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or has_sword(state, player)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) add_rule(world.get_location("Cube Cave - Holy Cross Chest"), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # can't ice grapple to this one, not enough space + set_rule(world.get_location("Quarry - [East] Bombable Wall"), lambda state: state.has(gun, player) or has_sword(state, player)) # Shop diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 72d4a498d1ee..bbceb7468ff3 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -68,3 +68,57 @@ def test_overworld_hc_chest(self) -> None: self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) self.collect_by_name(["Pages 42-43 (Holy Cross)"]) self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) + + +class TestERSpecial(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.IceGrappling.internal_name: options.IceGrappling.option_easy, + "plando_connections": [ + { + "entrance": "Stick House Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + { + "entrance": "Ziggurat Lower to Ziggurat Tower", + "exit": "Secret Gathering Place Exit" + } + ]} + # with these plando connections, you need to ice grapple from the back of lower zig to the front to get laurels + + +# ensure that ladder storage connections connect to the outlet region, not the portal's region +class TestLadderStorage(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.LadderStorage.internal_name: options.LadderStorage.option_hard, + options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false, + "plando_connections": [ + { + "entrance": "Fortress Courtyard Shop", + # "exit": "Ziggurat Portal Room Exit" + "exit": "Spawn to Far Shore" + }, + { + "entrance": "Fortress Courtyard to Beneath the Vault", + "exit": "Stick House Exit" + }, + { + "entrance": "Stick House Entrance", + "exit": "Fortress Courtyard to Overworld" + }, + { + "entrance": "Old House Waterfall Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + ]} + + def test_ls_to_shop_entrance(self) -> None: + self.collect_by_name(["Magic Orb"]) + self.assertFalse(self.can_reach_location("Fortress Courtyard - Page Near Cave")) + self.collect_by_name(["Pages 24-25 (Prayer)"]) + self.assertTrue(self.can_reach_location("Fortress Courtyard - Page Near Cave")) From d90cf0db656830d2f900884b36c2e1e6476a1fc1 Mon Sep 17 00:00:00 2001 From: neocerber <140952826+neocerber@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:46:34 -0400 Subject: [PATCH 236/393] SC2 EN/FR documentation update (#3440) * Draft of SC2 EN documentation update: added hotkey, known issues; enhanced goal and prog balancing description. Added place holder for changes to apply in the French documentation. * Enforced StarCraft over Starcraft, added information on locations in the FR documentation * Removed a mention to a no longer available third link in the required software (since download_data deprecated the need to do it by hand) * First version of FR campaign restriction for sc2; rewriting (FR/EN) of randomizer goal description * Finished description for sc2 AP goal , minor formating * Added, both en/fr, indications that logic is locations wise and not mission wise (i.e. you might need to dip) * Enforced the 120 carac limit to last commit * Removed mention of needing to use the weighted option page to exlcude unit/upgrades since it is not longer the case in AP v0.5.0 * Added mention of /received being different in SC2 client (both language). Added Known issues in the FR version. * Simplified the text a bit and corrected some errors * Enforced, again, Star-C-raft; setting -> option; applied sugg for readability enhancement --- worlds/sc2/docs/en_Starcraft 2.md | 85 +++++++++++++++++------ worlds/sc2/docs/fr_Starcraft 2.md | 34 +++++++++- worlds/sc2/docs/setup_en.md | 109 ++++++++++++++++++++++-------- worlds/sc2/docs/setup_fr.md | 20 +++--- 4 files changed, 188 insertions(+), 60 deletions(-) diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index 06464e3cd2fd..813fdb5f4a2b 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -1,4 +1,4 @@ -# Starcraft 2 +# StarCraft 2 ## Game page in other languages: * [Français](/games/Starcraft%202/info/fr) @@ -7,9 +7,11 @@ The following unlocks are randomized as items: 1. Your ability to build any non-worker unit. -2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss! +2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain +choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss! 3. Your ability to get the generic unit upgrades, such as attack and armour upgrades. -4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss. +4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades +for Zerg, and Spear of Adun upgrades for Protoss. 5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission. You find items by making progress in these categories: @@ -18,50 +20,91 @@ You find items by making progress in these categories: * Reaching milestones in the mission, such as completing part of a main objective * Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground -Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones. +In Archipelago's nomenclature, these are the locations where items can be found. +Each location, including mission completion, has a set of rules that specify the items required to access it. +These rules were designed assuming that StarCraft 2 is played on the Brutal difficulty. +Since each location has its own rule, it's possible that an item required for progression is in a mission where you +can't reach all of its locations or complete it. +However, mission completion is always required to gain access to new missions. + +Aside from mission completion, the other location categories can be disabled in the player options. +For instance, you can disable getting items for reaching required milestones. When you receive items, they will immediately become available, even during a mission, and you will be -notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client. +notified via a text box in the top-right corner of the game screen. +Item unlocks are also logged in the Archipelago client. -Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used. +Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab. +The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. +Additionally, metaprogression currencies such as credits and Solarite are not used. ## What is the goal of this game when randomized? -The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled. +The goal is to beat the final mission in the mission order. +The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four +StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled. +Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the +expected time to complete the world. +Note that the evolution missions from Heart of the Swarm are not included in the randomizer. -## What non-randomized changes are there from vanilla Starcraft 2? +## What non-randomized changes are there from vanilla StarCraft 2? 1. Some missions have more vespene geysers available to allow a wider variety of units. -2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas. -3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements. +2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, +brood war, and original ideas. +3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer +have tech requirements. 4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries. -5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build. -6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places. +5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors +taking longer to build. +6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them +without getting stuck in odd places. 7. Several vanilla bugs have been fixed. ## Which of my items can be in another player's world? -By default, any of StarCraft 2's items (specified above) can be in another player's world. See the -[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) -for more information on how to change this. +By default, any of StarCraft 2's items (specified above) can be in another player's world. +See the [Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this. ## Unique Local Commands -The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`. +The following commands are only available when using the StarCraft 2 Client to play with Archipelago. +You can list them any time in the client with `/help`. -* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files +* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. +Will overwrite existing files * `/difficulty [difficulty]` Overrides the difficulty set for the world. * Options: casual, normal, hard, brutal * `/game_speed [game_speed]` Overrides the game speed for the world * Options: default, slower, slow, normal, fast, faster * `/color [faction] [color]` Changes your color for one of your playable factions. * Faction options: raynor, kerrigan, primal, protoss, nova - * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default + * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, + brown, lightgreen, darkgrey, pink, rainbow, random, default * `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation. * Run without arguments to list all options. - * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc. -* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing. -* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided + * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource + amounts, controlling AI allies, etc. +* `/disable_mission_check` Disables the check to see if a mission is available to play. +Meant for co-op runs where one player can play the next mission in a chain the other player is doing. +* `/play [mission_id]` Starts a StarCraft 2 mission based off of the mission_id provided * `/available` Get what missions are currently available to play * `/unfinished` Get what missions are currently available to play and have not had all locations checked * `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails) + +Note that the behavior of the command `/received` was modified in the StarCraft 2 client. +In the Common client of Archipelago, the command returns the list of items received in the reverse order they were +received. +In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg). +Additionally, upgrades are grouped beneath their corresponding units or buildings. +A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown. +Every item whose name, race, or group name contains the provided parameter will be shown. + +## Known issues + +- StarCraft 2 Archipelago does not support loading a saved game. +For this reason, it is recommended to play on a difficulty level lower than what you are normally comfortable with. +- StarCraft 2 Archipelago does not support the restart of a mission from the StarCraft 2 menu. +To restart a mission, use the StarCraft 2 Client. +- A crash report is often generated when a mission is closed. +This does not affect the game and can be ignored. diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md index 4fcc8e689baa..092835c8e323 100644 --- a/worlds/sc2/docs/fr_Starcraft 2.md +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -21,6 +21,14 @@ Les *items* sont trouvés en accomplissant du progrès dans les catégories suiv * Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission *Devil's Playground* +Dans la nomenclature d'Archipelago, il s'agit des *locations* où l'on peut trouver des *items*. +Pour chaque *location*, incluant le fait de terminer une mission, il y a des règles qui définissent les *items* +nécessaires pour y accéder. +Ces règles ont été conçues en assumant que *StarCraft 2* est joué à la difficulté *Brutal*. +Étant donné que chaque *location* a ses propres règles, il est possible qu'un *item* nécessaire à la progression se +trouve dans une mission dont vous ne pouvez pas atteindre toutes les *locations* ou que vous ne pouvez pas terminer. +Cependant, il est toujours nécessaire de terminer une mission pour pouvoir accéder à de nouvelles missions. + Ces catégories, outre la première, peuvent être désactivées dans les options du jeu. Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont accomplies. @@ -37,8 +45,13 @@ Archipelago*. ## Quel est le but de ce jeu quand il est *randomized*? -Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.). -Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées. +Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.). +Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de +*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le +*mission order*. +Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles +peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde. +Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*. ## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2* @@ -93,3 +106,20 @@ mission de la chaîne qu'un autre joueur est en train d'entamer. l'accès à un *item* n'ont pas été accomplis. * `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la détection automatique de cette dernière échoue. + +Notez que le comportement de la commande `/received` a été modifié dans le client *StarCraft 2*. +Dans le client *Common* d'Archipelago, elle renvoie la liste des *items* reçus dans l'ordre inverse de leur réception. +Dans le client de *StarCraft 2*, la liste est divisée par races (i.e., *Any*, *Protoss*, *Terran*, et *Zerg*). +De plus, les améliorations sont regroupées sous leurs unités/bâtiments correspondants. +Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés. +Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés. + +## Problèmes connus + +- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde. +Pour cette raison, il est recommandé de jouer à un niveau de difficulté inférieur à celui avec lequel vous êtes +normalement à l'aise. +- *StarCraft 2 Archipelago* ne supporte pas le redémarrage d'une mission depuis le menu de *StarCraft 2*. +Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*. +- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée. +Cela n'affecte pas le jeu et peut être ignoré. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 991ed57e8741..5b378873f4a3 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -1,30 +1,39 @@ # StarCraft 2 Randomizer Setup Guide -This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where -to obtain a config file for StarCraft 2. +This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as +where to obtain a config file for StarCraft 2. ## Required Software - [StarCraft 2](https://starcraft2.com/en-us/) + - While StarCraft 2 Archipelago supports all four campaigns, they are not mandatory to play the randomizer. + If you do not own certain campaigns, you only need to exclude them in the configuration file of your world. - [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) ## How do I install this randomizer? -1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer. +1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the +Archipelago installer. - Linux users should also follow the instructions found at the bottom of this page (["Running in Linux"](#running-in-linux)). 2. Run ArchipelagoStarcraft2Client.exe. - - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. -3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step + only. +3. Type the command `/download_data`. +This will automatically install the Maps and Data files needed to play StarCraft 2 Archipelago. ## Where do I get a config file (aka "YAML") for this game? -Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options. +Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only +using default options. When you're setting up a multiworld, every world needs its own yaml file. There are three basic ways to get a yaml: -* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. -* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. +* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export +the yaml. +* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) +page or by generating it from the Launcher (`ArchipelagoLauncher.exe`). +The template includes descriptions of each option, you just have to edit it in your text editor of choice. * You can ask someone else to share their yaml to use it for yourself or adjust it as you wish. Remember the name you enter in the options page or in the yaml file, you'll need it to connect later! @@ -36,15 +45,31 @@ Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for The simplest way to check is to use the website [validator](/check). -You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. +You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the `Players/` folder +within your Archipelago installation and run `ArchipelagoGenerate.exe`. +You should see a new `.zip` file within the `output/` folder of your Archipelago installation if things worked +correctly. +It's advisable to run `ArchipelagoGenerate.exe` through a terminal so that you can see the printout, which will include +any errors and the precise output file name if it's successful. +If you don't like terminals, you can also check the log file in the `logs/` folder. #### What does Progression Balancing do? -For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off. +For StarCraft 2, this option doesn't have much impact. +It is an Archipelago option designed to balance world progression by swapping items in spheres. +If the Progression Balancing of one world is greater than that of others, items in that world are more likely to be +obtained early, and vice versa if its value is smaller. +However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little +influence on progression in a StarCraft 2 world. +StarCraft 2. +Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it +to zero) for a StarCraft 2 world. #### How do I specify items in a list, like in excluded items? -You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen: +You can look up the syntax for yaml collections in the +[YAML specification](https://yaml.org/spec/1.2.2/#21-collections). +For lists, every item goes on its own line, started with a hyphen: ```yaml excluded_items: @@ -52,11 +77,13 @@ excluded_items: - Drop-Pods (Kerrigan Tier 7) ``` -An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax. +An empty list is just a matching pair of square brackets: `[]`. +That's the default value in the template, which should let you know to use this syntax. #### How do I specify items for the starting inventory? -The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value: +The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. +The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value: ```yaml start_inventory: @@ -64,37 +91,61 @@ start_inventory: Additional Starting Vespene: 5 ``` -An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax. +An empty mapping is just a matching pair of curly braces: `{}`. +That's the default value in the template, which should let you know to use this syntax. #### How do I know the exact names of items and locations? -The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2. +The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations +for each game that it currently supports, including StarCraft 2. -You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. +You can also look up a complete list of the item names in the +[Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. This page also contains supplementary information of each item. -However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. +However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the +former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. -As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client. +As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over +the mission in the 'StarCraft 2 Launcher' tab in the client. ## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. - - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step + only. 2. Type `/connect [server ip]`. - If you're running through the website, the server IP should be displayed near the top of the room page. 3. Type your slot name from your YAML when prompted. 4. If the server has a password, enter that when prompted. -5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it! +5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your +world. +Unreachable missions will have greyed-out text. Just click on an available mission to start it! ## The game isn't launching when I try to start a mission. -First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out -the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a -specific description of what's going wrong and attach your log file to your message. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). +If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel +for help. +Please include a specific description of what's going wrong and attach your log file to your message. + +## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*. + +For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from +`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`. +If the folder doesn't exist, create it. + +To enable StarCraft 2 Archipelago to use your profile, follow these steps: +1. Launch StarCraft 2 via the Battle.net application. +2. Change your hotkey profile to the standard mode and accept. +3. Select your custom profile and accept. + +You will only need to do this once. ## Running in macOS -To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: +[macOS Guide](/tutorial/Archipelago/mac/en). +Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. ## Running in Linux @@ -102,9 +153,9 @@ To run StarCraft 2 through Archipelago in Linux, you will need to install the ga of the Archipelago client. Make sure you have StarCraft 2 installed using Wine, and that you have followed the -[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not -need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the -Lutris installer. +[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. +You will not need to copy the `.dll` files. +If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer. Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same @@ -139,5 +190,5 @@ below, replacing **${ID}** with the numerical ID. lutris lutris:rungameid/${ID} --output-script sc2.sh This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path -to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code -above into the existing script. +to the Wine binary that Lutris uses. +You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script. diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md index bb6c35bce1c7..d9b754572a66 100644 --- a/worlds/sc2/docs/setup_fr.md +++ b/worlds/sc2/docs/setup_fr.md @@ -6,6 +6,10 @@ indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago ## Logiciels requis - [*StarCraft 2*](https://starcraft2.com/en-us/) + - Bien que *StarCraft 2 Archipelago* supporte les quatre campagnes, elles ne sont pas obligatoires pour jouer au + *randomizer*. + Si vous ne possédez pas certaines campagnes, il vous suffit de les exclure dans le fichier de configuration de + votre monde. - [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) ## Comment est-ce que j'installe ce *randomizer*? @@ -41,10 +45,6 @@ préférences. Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml* puisque vous en aurez besoin pour vous connecter à votre monde! -Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de -certaines unités ou de leurs améliorations. -Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières. - Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago, veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml). @@ -66,15 +66,15 @@ dans le dossier `logs/`. #### À quoi sert l'option *Progression Balancing*? -Pour *Starcraft 2*, cette option ne fait pas grand-chose. +Pour *StarCraft 2*, cette option ne fait pas grand-chose. Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items* dans les *spheres*. Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes. -Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à +Cependant, *StarCraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à donc peu d'influence sur la progression dans *StarCraft 2*. Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à-d le définir à -zéro, pour *Starcraft 2*. +zéro, pour *StarCraft 2*. #### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*? @@ -122,6 +122,10 @@ Cependant, l'information présente dans cette dernière peut différer de celle puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui n'ont peut-être pas encore été inclus dans le site web d'Archipelago. +Pour ce qui concerne les *locations*, vous pouvez consulter tous les *locations* associés à une mission dans votre +monde en plaçant votre curseur sur la case correspondante dans l'onglet *StarCraft 2 Launcher* du client. + + ## Comment est-ce que je peux joindre un *MultiWorld*? 1. Exécuter `ArchipelagoStarcraft2Client.exe`. @@ -152,7 +156,7 @@ qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Docume Si le dossier n'existe pas, créez-le. Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes. -Lancez *Starcraft 2* via l'application *Battle.net*. +Lancez *StarCraft 2* via l'application *Battle.net*. Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil personnalisé et acceptez. Vous n'aurez besoin de faire ça qu'une seule fois. From 5021997df0997f0bd1151c6e5e523c38c4eafdac Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:13:01 -0500 Subject: [PATCH 237/393] Launcher: explicitly handle cli arguments to be passed to the Component (#3714) * adds handling for the `--` cli arg by having launcher capture, ignore, and pass through all of the values after it, while only processing (and validating) the values before it updates text client and its components to allow for args to be passed through, captured in run_as_textclient, and used in parse_args if present * Update worlds/LauncherComponents.py Co-authored-by: Aaron Wagener * explicitly using default args for parse_args when launched directly * revert manual arg parsing by request * Update CommonClient.py * Update LauncherComponents.py * :) --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 4 ++-- Launcher.py | 5 ++++- worlds/LauncherComponents.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 7f91172acf6c..122de476feca 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1035,7 +1035,7 @@ async def main(args): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable + args = parser.parse_args(args) if args.url: url = urllib.parse.urlparse(args.url) @@ -1053,4 +1053,4 @@ async def main(args): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING - run_as_textclient() + run_as_textclient(*sys.argv[1:]) # default value for parse_args diff --git a/Launcher.py b/Launcher.py index 97903e2ad103..42f93547cc9d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -401,7 +401,10 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): init_logging('Launcher') Utils.freeze_support() multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work - parser = argparse.ArgumentParser(description='Archipelago Launcher') + parser = argparse.ArgumentParser( + description='Archipelago Launcher', + usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]" + ) run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 4c64642abacb..fe6e44bb308e 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -61,7 +61,7 @@ def __repr__(self): processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: global processes import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) @@ -85,7 +85,7 @@ def __call__(self, path: str) -> bool: def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, "TextClient", args) + launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: From e4a5ed1cc45b4d58ba4ebdf095e5a990581bcee3 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:40:32 -0500 Subject: [PATCH 238/393] CommonClient: Explicitly parse url arg as an archipelago:// url (#3568) * Launcher "Text Client" --connect archipelago.gg:38281 should work, it doesn't, this fixes that * more explicit handling of expected values * removing launcher updates meaning this pr cannot stand alone but will not have merge issues later * add parser failure when an invalid url is found --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 122de476feca..911de4226dc3 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1039,11 +1039,14 @@ async def main(args): if args.url: url = urllib.parse.urlparse(args.url) - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) + if url.scheme == "archipelago": + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + else: + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") colorama.init() From cabfef669a74936000975d911105075b51a79595 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:46:58 -0400 Subject: [PATCH 239/393] Stardew Valley: Fix masteries logic so it requires levels and tools (#3640) * fix and add test * add test to make sure we check xp can be earned * fix python 3.8 test my god I hope it gets removed soon * fixing some review comments * curse you monstersanity * move month rule to has_level vanilla, so next level is in logic once the previous item is received * use progressive masteries to skills in test alsanity * rename reset_collection_state * add more tests around skill and masteries rules * progressive level issue --------- Co-authored-by: agilbert1412 --- worlds/stardew_valley/logic/skill_logic.py | 72 +++++++------- worlds/stardew_valley/rules.py | 12 ++- worlds/stardew_valley/test/__init__.py | 8 +- .../stardew_valley/test/rules/TestSkills.py | 97 ++++++++++++++++--- 4 files changed, 137 insertions(+), 52 deletions(-) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 4d5567302afe..17fabca28d95 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -15,13 +15,13 @@ 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_, False_, true_, And +from ..stardew_rule import StardewRule, true_, True_, False_ from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance from ..strings.quality_names import ForageQuality from ..strings.region_names import Region -from ..strings.skill_names import Skill, all_mod_skills +from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills from ..strings.tool_names import ToolMaterial, Tool from ..strings.wallet_item_names import Wallet @@ -43,22 +43,17 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - tool_level = (level - 1) // 2 + tool_level = min(4, (level - 1) // 2) tool_material = ToolMaterial.tiers[tool_level] - months = max(1, level - 1) - months_rule = self.logic.time.has_lived_months(months) - 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 = self.logic.skill.has_previous_level(skill, level) if skill == Skill.fishing: xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: 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.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + 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) | \ @@ -70,22 +65,34 @@ 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 previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") - return previous_level_rule & months_rule & xp_rule + return previous_level_rule & xp_rule # Should be cached def has_level(self, skill: str, level: int) -> StardewRule: - if level <= 0: - return True_() + assert level >= 0, f"There is no level before level 0." + if level == 0: + return true_ if self.options.skill_progression == options.SkillProgression.option_vanilla: return self.logic.skill.can_earn_level(skill, level) return self.logic.received(f"{skill} Level", level) + def has_previous_level(self, skill: str, level: int) -> StardewRule: + assert level > 0, f"There is no level before level 0." + if level == 1: + return true_ + + if self.options.skill_progression == options.SkillProgression.option_vanilla: + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) + + return self.logic.received(f"{skill} Level", level - 1) + @cache_self1 def has_farming_level(self, level: int) -> StardewRule: return self.logic.skill.has_level(Skill.farming, level) @@ -108,18 +115,9 @@ 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() + def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills) + return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills)) @cached_property def can_get_farming_xp(self) -> StardewRule: @@ -197,13 +195,19 @@ def can_forage_quality(self, quality: str) -> StardewRule: 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 can_earn_mastery(self, skill: str) -> StardewRule: + # Checking for level 11, so it includes having level 10 and being able to earn xp. + return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) 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") + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(f"{skill} Mastery") + + return self.logic.skill.can_earn_mastery(skill) + + @cached_property + 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_any_skills_maxed(included_modded_skills=False) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 89b1cf87c3c1..e9bdd8c25bbb 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -154,7 +154,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw 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}" + 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) @@ -168,13 +168,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta mods = world_options.mods if world_options.skill_progression == SkillProgression.option_vanilla: return + 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: + + if world_options.skill_progression == SkillProgression.option_progressive: 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) + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill)) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -256,8 +259,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S 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, 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) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 4dee0ebf6d66..e7278cba2800 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x(): 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.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, 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, @@ -310,6 +310,12 @@ def create_item(self, item: str) -> StardewItem: self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item + def remove_one_by_name(self, item: str) -> None: + self.remove(self.create_item(item)) + + def reset_collection_state(self): + self.multiworld.state = self.original_state.copy() + pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py index 1c6874f31529..77adade886dc 100644 --- a/worlds/stardew_valley/test/rules/TestSkills.py +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -1,23 +1,30 @@ -from ... import HasProgressionPercent +from ... import HasProgressionPercent, StardewLogic from ...options import ToolProgression, SkillProgression, Mods -from ...strings.skill_names import all_skills +from ...strings.skill_names import all_skills, all_vanilla_skills, Skill from ...test import SVTestBase -class TestVanillaSkillLogicSimplification(SVTestBase): +class TestSkillProgressionVanilla(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)) + rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent)) + def test_has_mastery_requires_month_equivalent_to_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + time_rule = logic.time.has_lived_months(10) -class TestAllSkillsRequirePrevious(SVTestBase): + self.assertIn(time_rule, rule.current_rules) + + +class TestSkillProgressionProgressive(SVTestBase): options = { - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + SkillProgression.internal_name: SkillProgression.option_progressive, Mods.internal_name: frozenset(Mods.valid_keys), } @@ -25,16 +32,82 @@ 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}" + location = self.multiworld.get_location(location_name, self.player) + with self.subTest(location_name): - can_reach = self.can_reach_location(location_name) if level > 1: - self.assertFalse(can_reach) + self.assert_reach_location_false(location, self.multiworld.state) self.collect(f"{skill} Level") - can_reach = self.can_reach_location(location_name) - self.assertTrue(can_reach) - self.multiworld.state = self.original_state.copy() + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_has_level_requires_exact_amount_of_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 8) + + self.assertEqual(level_rule, rule) + + def test_has_previous_level_requires_one_less_level_than_requested(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_previous_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 7) + + self.assertEqual(level_rule, rule) + + def test_has_mastery_requires_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + level_rule = logic.received("Farming Level", 10) + + self.assertIn(level_rule, rule.current_rules) + + +class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ToolProgression.internal_name: ToolProgression.option_progressive, + Mods.internal_name: frozenset(), + } + + def test_has_mastery_requires_the_item(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + received_mastery = logic.received("Farming Mastery") + + self.assertEqual(received_mastery, rule) + + def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self): + self.collect_everything() + + for skill in all_vanilla_skills: + with self.subTest(skill): + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + for skill in all_vanilla_skills: + with self.subTest(skill): + self.collect_everything() + self.remove_one_by_name(f"{skill} Level") + + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + self.collect_everything() + self.remove_one_by_name(f"Progressive Pickaxe") + location = self.multiworld.get_location("Mining Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + self.reset_collection_state() From 05b257adf9bd9300acd4ff5584f6087f70716ad1 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Sep 2024 09:48:48 -0700 Subject: [PATCH 240/393] Pokemon Emerald: Make use of `NamedTuple._replace` (#3727) --- worlds/pokemon_emerald/data.py | 6 ++---- worlds/pokemon_emerald/opponents.py | 6 +++--- worlds/pokemon_emerald/pokemon.py | 31 +++++++++++------------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index d89ab5febb33..432d59387391 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES -@dataclass -class TrainerPokemonData: +class TrainerPokemonData(NamedTuple): species_id: int level: int moves: Optional[Tuple[int, int, int, int]] -@dataclass -class TrainerPartyData: +class TrainerPartyData(NamedTuple): pokemon: List[TrainerPokemonData] pokemon_data_type: TrainerPokemonDataTypeEnum address: int diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 09e947546d7c..966d19205447 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Dict, List, Set -from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data +from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data from .options import RandomizeTrainerParties from .pokemon import filter_species_by_nearby_bst from .util import int_to_bool_array @@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3] ) - new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves)) - trainer.party.pokemon = new_party + trainer.party = trainer.party._replace(pokemon=new_party) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index c60e5e9d4f14..fec1101dab0d 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -4,8 +4,7 @@ import functools from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple -from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, - SpeciesData, data) +from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data) from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, TmTutorCompatibility) @@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: type_bias, normal_bias, species.types) else: new_move = 0 - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 # All moves from here onward are actual moves. @@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: new_move = get_random_move(world.random, {move.move_id for move in new_learnset} | world.blacklisted_moves, type_bias, normal_bias, species.types) - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 species.learnset = new_learnset @@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None: picked_evolution = world.random.choice(potential_evolutions) for trainer_name, starter_position, is_evolved in rival_teams[i]: + new_species_id = picked_evolution if is_evolved else starter.species_id trainer_data = world.modified_trainers[data.constants[trainer_name]] - trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + trainer_data.party.pokemon[starter_position] = \ + trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id) def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: @@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: world.random.shuffle(shuffled_species) for i, encounter in enumerate(data.legendary_encounters): - world.modified_legendary_encounters.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.legendary_encounters in { RandomizeLegendaryEncounters.option_match_base_stats, @@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: if should_match_bst: candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) - world.modified_legendary_encounters.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_legendary_encounters.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) @@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: world.modified_misc_pokemon = [] for i, encounter in enumerate(data.misc_pokemon): - world.modified_misc_pokemon.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.misc_pokemon in { RandomizeMiscPokemon.option_match_base_stats, @@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: if len(player_filtered_candidates) > 0: candidates = player_filtered_candidates - world.modified_misc_pokemon.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_misc_pokemon.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) From 6d6d35d598dbb984b7b5cccf6567d5dc9e4ddd7d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:50:08 -0500 Subject: [PATCH 241/393] Rogue Legacy: Update to Options API (#3755) * fix deprecation * multiworld.random -> world.random * Various small fixes --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic --- worlds/rogue_legacy/Options.py | 78 ++++++++++--------- worlds/rogue_legacy/Regions.py | 31 ++++---- worlds/rogue_legacy/Rules.py | 50 ++++++------ worlds/rogue_legacy/__init__.py | 111 +++++++++++++-------------- worlds/rogue_legacy/test/__init__.py | 2 +- 5 files changed, 139 insertions(+), 133 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index d8298c85c8fb..9210082f7317 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet +from dataclasses import dataclass class StartingGender(Choice): @@ -336,42 +336,44 @@ class AvailableClasses(OptionSet): The upgraded form of your starting class will be available regardless. """ display_name = "Available Classes" - default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + default = frozenset( + {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + ) valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} -rl_options: Dict[str, type(Option)] = { - "starting_gender": StartingGender, - "starting_class": StartingClass, - "available_classes": AvailableClasses, - "new_game_plus": NewGamePlus, - "fairy_chests_per_zone": FairyChestsPerZone, - "chests_per_zone": ChestsPerZone, - "universal_fairy_chests": UniversalFairyChests, - "universal_chests": UniversalChests, - "vendors": Vendors, - "architect": Architect, - "architect_fee": ArchitectFee, - "disable_charon": DisableCharon, - "require_purchasing": RequirePurchasing, - "progressive_blueprints": ProgressiveBlueprints, - "gold_gain_multiplier": GoldGainMultiplier, - "number_of_children": NumberOfChildren, - "free_diary_on_generation": FreeDiaryOnGeneration, - "khidr": ChallengeBossKhidr, - "alexander": ChallengeBossAlexander, - "leon": ChallengeBossLeon, - "herodotus": ChallengeBossHerodotus, - "health_pool": HealthUpPool, - "mana_pool": ManaUpPool, - "attack_pool": AttackUpPool, - "magic_damage_pool": MagicDamageUpPool, - "armor_pool": ArmorUpPool, - "equip_pool": EquipUpPool, - "crit_chance_pool": CritChanceUpPool, - "crit_damage_pool": CritDamageUpPool, - "allow_default_names": AllowDefaultNames, - "additional_lady_names": AdditionalNames, - "additional_sir_names": AdditionalNames, - "death_link": DeathLink, -} +@dataclass +class RLOptions(PerGameCommonOptions): + starting_gender: StartingGender + starting_class: StartingClass + available_classes: AvailableClasses + new_game_plus: NewGamePlus + fairy_chests_per_zone: FairyChestsPerZone + chests_per_zone: ChestsPerZone + universal_fairy_chests: UniversalFairyChests + universal_chests: UniversalChests + vendors: Vendors + architect: Architect + architect_fee: ArchitectFee + disable_charon: DisableCharon + require_purchasing: RequirePurchasing + progressive_blueprints: ProgressiveBlueprints + gold_gain_multiplier: GoldGainMultiplier + number_of_children: NumberOfChildren + free_diary_on_generation: FreeDiaryOnGeneration + khidr: ChallengeBossKhidr + alexander: ChallengeBossAlexander + leon: ChallengeBossLeon + herodotus: ChallengeBossHerodotus + health_pool: HealthUpPool + mana_pool: ManaUpPool + attack_pool: AttackUpPool + magic_damage_pool: MagicDamageUpPool + armor_pool: ArmorUpPool + equip_pool: EquipUpPool + crit_chance_pool: CritChanceUpPool + crit_damage_pool: CritDamageUpPool + allow_default_names: AllowDefaultNames + additional_lady_names: AdditionalNames + additional_sir_names: AdditionalNames + death_link: DeathLink diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py index 5d07fccbc4d4..61b0ef73ec78 100644 --- a/worlds/rogue_legacy/Regions.py +++ b/worlds/rogue_legacy/Regions.py @@ -1,15 +1,18 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import MultiWorld, Region, Entrance from .Locations import RLLocation, location_table, get_locations_by_category +if TYPE_CHECKING: + from . import RLWorld + class RLRegionData(NamedTuple): locations: Optional[List[str]] region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "RLWorld"): regions: Dict[str, RLRegionData] = { "Menu": RLRegionData(None, ["Castle Hamson"]), "The Manor": RLRegionData([], []), @@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["The Fountain Room"].locations.append("Fountain Room") # Chests - chests = int(multiworld.chests_per_zone[player]) + chests = int(world.options.chests_per_zone) for i in range(0, chests): - if multiworld.universal_chests[player]: + if world.options.universal_chests: regions["Castle Hamson"].locations.append(f"Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") @@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") # Fairy Chests - chests = int(multiworld.fairy_chests_per_zone[player]) + chests = int(world.options.fairy_chests_per_zone) for i in range(0, chests): - if multiworld.universal_fairy_chests[player]: + if world.options.universal_fairy_chests: regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") @@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int): # Set up the regions correctly. for name, data in regions.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) - - multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player)) - multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player)) - multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player)) - multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player)) - multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player)) - multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player)) + world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) + + world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) + world.get_entrance("The Manor").connect(world.get_region("The Manor")) + world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) + world.get_entrance("The Maya").connect(world.get_region("The Maya")) + world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) + world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 2fac8d561399..505bbdd63541 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -1,9 +1,13 @@ -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import RLWorld -def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: - return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \ - int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player]) + +def get_upgrade_total(world: "RLWorld") -> int: + return int(world.options.health_pool) + int(world.options.mana_pool) + \ + int(world.options.attack_pool) + int(world.options.magic_damage_pool) def get_upgrade_count(state: CollectionState, player: int) -> int: @@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool return get_upgrade_count(state, player) >= amount -def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool: - return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100))) +def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: + return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) def has_movement_rune(state: CollectionState, player: int) -> bool: @@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool: return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "RLWorld", player: int): # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if multiworld.vendors[player] == "normal": - multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \ + if world.options.vendors == "normal": + world.get_location("Forest Abkhazia Boss Reward").access_rule = \ lambda state: has_vendors(state, player) # Gate each manor location so everything isn't dumped into sphere 1. manor_rules = { - "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [ + "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ "Manor - Left Wing Window", "Manor - Left Wing Rooftop", "Manor - Right Wing Window", @@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Left Tree 2", "Manor - Right Tree", ], - "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [ + "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ "Manor - Left Big Upper 1", "Manor - Left Big Upper 2", "Manor - Left Big Windows", @@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Right Big Rooftop", "Manor - Right Extension", ], - "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [ + "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ "Manor - Right High Base", "Manor - Right High Upper", "Manor - Right High Tower", @@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int): # Set rules for manor locations. for event, locations in manor_rules.items(): for location in locations: - multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player) + world.get_location(location).access_rule = lambda state: state.has(event, player) # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]: + for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: fairy_location.access_rule = lambda state: has_fairy_progression(state, player) # Region rules. - multiworld.get_entrance("Forest Abkhazia", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player) + world.get_entrance("Forest Abkhazia").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - multiworld.get_entrance("The Maya", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player) + world.get_entrance("The Maya").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - multiworld.get_entrance("Land of Darkness", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player) + world.get_entrance("Land of Darkness").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - multiworld.get_entrance("The Fountain Room", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player) + world.get_entrance("The Fountain Room").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) # Win condition. - multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) + world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 78e56a794c85..290f4a60ac21 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -4,7 +4,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table -from .Options import rl_options +from .Options import RLOptions from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -33,20 +33,17 @@ class RLWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game = "Rogue Legacy" - option_definitions = rl_options + options_dataclass = RLOptions + options: RLOptions topology_present = True required_client_version = (0, 3, 5) web = RLWeb() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.code for name, data in location_table.items()} - - # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged. - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} + location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in rl_options} + return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): location_ids_used_per_game = { @@ -74,18 +71,18 @@ def generate_early(self): ) # Check validation of names. - additional_lady_names = len(self.get_setting("additional_lady_names").value) - additional_sir_names = len(self.get_setting("additional_sir_names").value) - if not self.get_setting("allow_default_names"): - if additional_lady_names < int(self.get_setting("number_of_children")): + additional_lady_names = len(self.options.additional_lady_names.value) + additional_sir_names = len(self.options.additional_sir_names.value) + if not self.options.allow_default_names: + if additional_lady_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - if additional_sir_names < int(self.get_setting("number_of_children")): + if additional_sir_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") def create_items(self): item_pool: List[RLItem] = [] @@ -95,110 +92,110 @@ def create_items(self): # Architect if name == "Architect": - if self.get_setting("architect") == "disabled": + if self.options.architect == "disabled": continue - if self.get_setting("architect") == "start_unlocked": + if self.options.architect == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("architect") == "early": + if self.options.architect == "early": self.multiworld.local_early_items[self.player]["Architect"] = 1 # Blacksmith and Enchantress if name == "Blacksmith" or name == "Enchantress": - if self.get_setting("vendors") == "start_unlocked": + if self.options.vendors == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("vendors") == "early": + if self.options.vendors == "early": self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 self.multiworld.local_early_items[self.player]["Enchantress"] = 1 # Haggling - if name == "Haggling" and self.get_setting("disable_charon"): + if name == "Haggling" and self.options.disable_charon: continue # Blueprints if data.category == "Blueprints": # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"): + if name == "Progressive Blueprints" and not self.options.progressive_blueprints: continue # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"): + elif name != "Progressive Blueprints" and self.options.progressive_blueprints: continue # Classes if data.category == "Classes": if name == "Progressive Knights": - if "Knight" not in self.get_setting("available_classes"): + if "Knight" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knight": + if self.options.starting_class == "knight": quantity = 1 if name == "Progressive Mages": - if "Mage" not in self.get_setting("available_classes"): + if "Mage" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "mage": + if self.options.starting_class == "mage": quantity = 1 if name == "Progressive Barbarians": - if "Barbarian" not in self.get_setting("available_classes"): + if "Barbarian" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "barbarian": + if self.options.starting_class == "barbarian": quantity = 1 if name == "Progressive Knaves": - if "Knave" not in self.get_setting("available_classes"): + if "Knave" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knave": + if self.options.starting_class == "knave": quantity = 1 if name == "Progressive Miners": - if "Miner" not in self.get_setting("available_classes"): + if "Miner" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "miner": + if self.options.starting_class == "miner": quantity = 1 if name == "Progressive Shinobis": - if "Shinobi" not in self.get_setting("available_classes"): + if "Shinobi" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "shinobi": + if self.options.starting_class == "shinobi": quantity = 1 if name == "Progressive Liches": - if "Lich" not in self.get_setting("available_classes"): + if "Lich" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "lich": + if self.options.starting_class == "lich": quantity = 1 if name == "Progressive Spellthieves": - if "Spellthief" not in self.get_setting("available_classes"): + if "Spellthief" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "spellthief": + if self.options.starting_class == "spellthief": quantity = 1 if name == "Dragons": - if "Dragon" not in self.get_setting("available_classes"): + if "Dragon" not in self.options.available_classes: continue if name == "Traitors": - if "Traitor" not in self.get_setting("available_classes"): + if "Traitor" not in self.options.available_classes: continue # Skills if name == "Health Up": - quantity = self.get_setting("health_pool") + quantity = self.options.health_pool.value elif name == "Mana Up": - quantity = self.get_setting("mana_pool") + quantity = self.options.mana_pool.value elif name == "Attack Up": - quantity = self.get_setting("attack_pool") + quantity = self.options.attack_pool.value elif name == "Magic Damage Up": - quantity = self.get_setting("magic_damage_pool") + quantity = self.options.magic_damage_pool.value elif name == "Armor Up": - quantity = self.get_setting("armor_pool") + quantity = self.options.armor_pool.value elif name == "Equip Up": - quantity = self.get_setting("equip_pool") + quantity = self.options.equip_pool.value elif name == "Crit Chance Up": - quantity = self.get_setting("crit_chance_pool") + quantity = self.options.crit_chance_pool.value elif name == "Crit Damage Up": - quantity = self.get_setting("crit_damage_pool") + quantity = self.options.crit_damage_pool.value # Ignore filler, it will be added in a later stage. if data.category == "Filler": @@ -215,7 +212,7 @@ def create_items(self): def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] - return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] + return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] def create_item(self, name: str) -> RLItem: data = item_table[name] @@ -226,10 +223,10 @@ def create_event(self, name: str) -> RLItem: return RLItem(name, data.classification, data.code, self.player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self, self.player) def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self) self._place_events() def _place_events(self): @@ -238,7 +235,7 @@ def _place_events(self): self.create_event("Defeat The Fountain")) # Khidr / Neo Khidr - if self.get_setting("khidr") == "vanilla": + if self.options.khidr == "vanilla": self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( self.create_event("Defeat Khidr")) else: @@ -246,7 +243,7 @@ def _place_events(self): self.create_event("Defeat Neo Khidr")) # Alexander / Alexander IV - if self.get_setting("alexander") == "vanilla": + if self.options.alexander == "vanilla": self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( self.create_event("Defeat Alexander")) else: @@ -254,7 +251,7 @@ def _place_events(self): self.create_event("Defeat Alexander IV")) # Ponce de Leon / Ponce de Freon - if self.get_setting("leon") == "vanilla": + if self.options.leon == "vanilla": self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( self.create_event("Defeat Ponce de Leon")) else: @@ -262,7 +259,7 @@ def _place_events(self): self.create_event("Defeat Ponce de Freon")) # Herodotus / Astrodotus - if self.get_setting("herodotus") == "vanilla": + if self.options.herodotus == "vanilla": self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( self.create_event("Defeat Herodotus")) else: diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py index 2639e618c678..3346476ba644 100644 --- a/worlds/rogue_legacy/test/__init__.py +++ b/worlds/rogue_legacy/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class RLTestBase(WorldTestBase): From cf375cbcc4c399290b7ebc893e10992029761230 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 12:54:27 -0500 Subject: [PATCH 242/393] Core: Fix Generate's slot parsing to default unknown slot names to file name (#3795) * make Generate handle slots without names defined better * set name dict before loop so we don't have to check for its existence later * move setter so it's more obvious why --- Generate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index 6220c0eb8188..4eba05cc52fe 100644 --- a/Generate.py +++ b/Generate.py @@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output + erargs.name = {} settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" - elif not erargs.name[player]: # if name was not specified, generate it from filename + elif player not in erargs.name: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) From 5a5162c9d3a93295eccaad74fe28226f5cc0342f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 8 Sep 2024 12:55:17 -0500 Subject: [PATCH 243/393] The Messenger: improve automated installation (#3083) * add deck support to the messenger mod setup * Add tkinter cleanup because it's janky * prompt about launching the game instead of just doing it * add "better" file validation to courier checking * make it a bit more palatable * make it a bit more palatable * add the executable's md5 to ensure the correct file is selected * handle a bad md5 and show a message * make the utils wrapper snake_case and add a docstring * use stored archive instead of head * don't give other people the convenience method ig --- worlds/messenger/__init__.py | 1 + worlds/messenger/client_setup.py | 106 +++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 1bca3a37ad71..9a38953ffbdf 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -27,6 +27,7 @@ class MessengerSettings(Group): class GamePath(FilePath): description = "The Messenger game executable" is_exe = True + md5s = ["1b53534569060bc06179356cd968ed1d"] game_path: GamePath = GamePath("TheMessenger.exe") diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 6bff78df364d..77a0f634326c 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -5,7 +5,6 @@ import subprocess import urllib.request from shutil import which -from tkinter.messagebox import askyesnocancel from typing import Any, Optional from zipfile import ZipFile from Utils import open_file @@ -18,11 +17,33 @@ MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" +def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: + """ + Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. + + :param title: Title to be displayed at the top of the message box. + :param text: Text to be displayed inside the message box. + :return: Returns True if yes, False if no, None if cancel. + """ + from tkinter import Tk, messagebox + root = Tk() + root.withdraw() + ret = messagebox.askyesnocancel(title, text) + root.update() + return ret + + + def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" - return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll")) + assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") + with open(assembly_path, "rb") as assembly: + for line in assembly: + if b"Courier" in line: + return True + return False def mod_installed() -> bool: """Check if the mod is installed""" @@ -57,27 +78,34 @@ def install_courier() -> None: if not is_windows: mono_exe = which("mono") if not mono_exe: - # steam deck support but doesn't currently work - messagebox("Failure", "Failed to install Courier", True) - raise RuntimeError("Failed to install Courier") - # # download and use mono kickstart - # # this allows steam deck support - # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip" - # target = os.path.join(folder, "monoKickstart") - # os.makedirs(target, exist_ok=True) - # with urllib.request.urlopen(mono_kick_url) as download: - # with ZipFile(io.BytesIO(download.read()), "r") as zf: - # for member in zf.infolist(): - # zf.extract(member, path=target) - # installer = subprocess.Popen([os.path.join(target, "precompiled"), - # os.path.join(folder, "MiniInstaller.exe")], shell=False) - # os.remove(target) + # download and use mono kickstart + # this allows steam deck support + mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip" + files = [] + with urllib.request.urlopen(mono_kick_url) as download: + with ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + if "precompiled/" not in member.filename or member.filename.endswith("/"): + continue + member.filename = member.filename.split("/")[-1] + if member.filename.endswith("bin.x86_64"): + member.filename = "MiniInstaller.bin.x86_64" + zf.extract(member, path=game_folder) + files.append(member.filename) + mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64") + os.chmod(mono_installer, 0o755) + installer = subprocess.Popen(mono_installer, shell=False) + failure = installer.wait() + for file in files: + os.remove(file) else: - installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False) + installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True) + failure = installer.wait() else: - installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False) + installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True) + failure = installer.wait() - failure = installer.wait() + print(failure) if failure: messagebox("Failure", "Failed to install Courier", True) os.chdir(working_directory) @@ -125,18 +153,35 @@ def available_mod_update(latest_version: str) -> bool: return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) from . import MessengerWorld - game_folder = os.path.dirname(MessengerWorld.settings.game_path) + try: + game_folder = os.path.dirname(MessengerWorld.settings.game_path) + except ValueError as e: + logging.error(e) + messagebox("Invalid File", "Selected file did not match expected hash. " + "Please try again and ensure you select The Messenger.exe.") + return working_directory = os.getcwd() + # setup ssl context + try: + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + context.set_alpn_protocols(["http/1.1"]) + https_handler = urllib.request.HTTPSHandler(context=context) + opener = urllib.request.build_opener(https_handler) + urllib.request.install_opener(opener) + except ImportError: + pass if not courier_installed(): - should_install = askyesnocancel("Install Courier", - "No Courier installation detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Courier", + "No Courier installation detected. Would you like to install now?") if not should_install: return logging.info("Installing Courier") install_courier() if not mod_installed(): - should_install = askyesnocancel("Install Mod", - "No randomizer mod detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Mod", + "No randomizer mod detected. Would you like to install now?") if not should_install: return logging.info("Installing Mod") @@ -144,17 +189,24 @@ def available_mod_update(latest_version: str) -> bool: else: latest = request_data(MOD_URL)["tag_name"] if available_mod_update(latest): - should_update = askyesnocancel("Update Mod", - f"New mod version detected. Would you like to update to {latest} now?") + should_update = ask_yes_no_cancel("Update Mod", + f"New mod version detected. Would you like to update to {latest} now?") if should_update: logging.info("Updating mod") install_mod() elif should_update is None: return + if not args: + should_launch = ask_yes_no_cancel("Launch Game", + "Mod installed and up to date. Would you like to launch the game now?") + if not should_launch: + return + parser = argparse.ArgumentParser(description="Messenger Client Launcher") parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") args = parser.parse_args(args) + if not is_windows: if args.url: open_file(f"steam://rungameid/764790//{args.url}/") From e52ce0149a0470b594ce6f675dbd5b0bb7c994c0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:57:09 -0400 Subject: [PATCH 244/393] Rogue Legacy: Split Additional Names into two option classes #3908 --- worlds/rogue_legacy/Options.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index 9210082f7317..139ff6094427 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -175,13 +175,21 @@ class NumberOfChildren(Range): default = 3 -class AdditionalNames(OptionSet): +class AdditionalLadyNames(OptionSet): """ Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list of names your children can have. The first value will also be your initial character's name depending on Starting Gender. """ - display_name = "Additional Names" + display_name = "Additional Lady Names" + +class AdditionalSirNames(OptionSet): + """ + Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list + of names your children can have. The first value will also be your initial character's name depending on Starting + Gender. + """ + display_name = "Additional Sir Names" class AllowDefaultNames(DefaultOnToggle): @@ -374,6 +382,6 @@ class RLOptions(PerGameCommonOptions): crit_chance_pool: CritChanceUpPool crit_damage_pool: CritDamageUpPool allow_default_names: AllowDefaultNames - additional_lady_names: AdditionalNames - additional_sir_names: AdditionalNames + additional_lady_names: AdditionalLadyNames + additional_sir_names: AdditionalSirNames death_link: DeathLink From 4aab317665d5d2d9a72d2bc5ea9446639eaf887e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:56:15 -0500 Subject: [PATCH 245/393] ALTTP: Plando (#2904) fixes (#3834) --- Options.py | 14 ++++++++++++++ worlds/alttp/Options.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index ecde6275f1ea..b79714635d9e 100644 --- a/Options.py +++ b/Options.py @@ -973,7 +973,19 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -981,6 +993,8 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 20dd18038a14..bd87cbf2c3ea 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections): entrances = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - exits = set([connection[1] for connection in ( + exits = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) From 09c7f5f909e6ca23de5bc58f8174a1794b07f817 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:27 +0200 Subject: [PATCH 246/393] The Witness: Bump Required Client Version (#3891) The newest release of the Witness client connects with 0.5.1 https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/tag/7.0.0p10 --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index cdb17a483b1e..b4b38c883e7d 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -61,7 +61,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 1) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations From 170aedba8fbe35765289a8628b89acf9fd1515ec Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:47 +0200 Subject: [PATCH 247/393] The Witness: Fix hints always displaying the Witness player (#3861) * The Witness: Fix hints always displaying the Witness player Got a bit too trigger happy with changing instances of `world.multiworld.player_name` to `world.player_name` - Some of these were actually *supposed* to be other players. Alternate title: The Witness doesn't have a Silph Scope * that one i guess --- worlds/witness/hints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 2c5f816b2bc2..99e8eea2eb89 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.player_name + ")" + location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.player_name + ")" + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" hint_text = "" area: Optional[str] = None From 7ff201e32c859eeb1b3e07ee087f11da3249f833 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Tue, 10 Sep 2024 17:01:36 +0200 Subject: [PATCH 248/393] Yacht Dice: add get_filler_item_name (#3916) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index 3a79eff04046..d86ee3382d33 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -466,6 +466,9 @@ def create_regions(self): menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] + + def get_filler_item_name(self) -> str: + return "Good RNG" def set_rules(self): """ From 874392756b706bc07f4c1ff9429ed0b16e52abd3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:20:07 -0700 Subject: [PATCH 249/393] Pokemon Emerald: Add normalize encounter rate option to slot data (#3917) --- worlds/pokemon_emerald/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f572f..d281dde23cb0 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -711,6 +711,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "trainersanity", "modify_118", "death_link", + "normalize_encounter_rates", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements From c9f1a21bd2b8888e5b4dc75123c19b0a016ee261 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:22:04 -0700 Subject: [PATCH 250/393] BizHawkClient: Remove `run_gui` in favor of `make_gui` (#3910) --- CommonClient.py | 2 +- worlds/_bizhawk/context.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 911de4226dc3..6bdd8fc819da 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,7 +662,7 @@ def handle_connection_loss(self, msg: str) -> None: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> type: + def make_gui(self) -> typing.Type["kvui.GameManager"]: """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 234faf3b65cf..896c8fb7b504 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -59,14 +59,10 @@ def __init__(self, server_address: Optional[str], password: Optional[str]): self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 - def run_gui(self): - from kvui import GameManager - - class BizHawkManager(GameManager): - base_title = "Archipelago BizHawk Client" - - self.ui = BizHawkManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def make_gui(self): + ui = super().make_gui() + ui.base_title = "Archipelago BizHawk Client" + return ui def on_package(self, cmd, args): if cmd == "Connected": From 7621889b8b626e89947d6258ddd5ade65d434ddb Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 11 Sep 2024 04:22:53 -0700 Subject: [PATCH 251/393] DS3: Add nex3 as a world maintainer (#3882) I've already discussed this with @Marechal-L and gotten his approval. --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 28dcc6736283..ee7fd7ed863b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,7 +46,7 @@ /worlds/clique/ @ThePhar # Dark Souls III -/worlds/dark_souls_3/ @Marechal-L +/worlds/dark_souls_3/ @Marechal-L @nex3 # Donkey Kong Country 3 /worlds/dkc3/ @PoryGone From ed948e3e5b60ea67d126b7ef06a60dcccd71f4aa Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 13 Sep 2024 15:02:13 +0100 Subject: [PATCH 252/393] sm64ex: Add missing indirect condition for BitFS randomized entrance (#3926) The Bowser in the Fire Sea randomized entrance has an access rule that requires being able to reach "DDD: Board Bowser's Sub", but being able to reach a location also requires being able to reach the region that location is in, so an indirect condition is required. --- worlds/sm64ex/Regions.py | 4 ++-- worlds/sm64ex/Rules.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 6fc2d74b96dc..52126bcf9ff7 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int): regBitS.subregions = [bits_top] -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): +def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance: sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - sourceRegion.connect(targetRegion, rule=rule) + return sourceRegion.connect(targetRegion, rule=rule) def create_region(name: str, player: int, world: MultiWorld) -> SM64Region: diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 9add8d9b2932..1535f9ca1fde 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], rf.build_rule("GP")) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], - lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + # Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that + # needs to be registered is its parent region. + world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) From 5530d181da643beb96abd915c259f6c22cb9dc7f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 16 Sep 2024 06:48:13 +0200 Subject: [PATCH 253/393] Core: update version number (#3944) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index f89330cf7c65..d6709431d32c 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.0" +__version__ = "0.5.1" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From 84805a4e541c6dc2a0d95b8c3609b1faf9c240db Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 16 Sep 2024 08:30:47 -0400 Subject: [PATCH 254/393] HK: XBox doesn't exist #3932 --- worlds/hk/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index c046785038d8..21cdcb68b3a9 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -15,7 +15,7 @@ ### What to do if Lumafly fails to find your installation directory 1. Find the directory manually. * Xbox Game Pass: - 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. + 1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar. 2. Click the three points then click "Manage". 3. Go to the "Files" tab and select "Browse...". 4. Click "Hollow Knight", then "Content", then click the path bar and copy it. From ee12dda3611cdf016d8e8a8633a32333a3f47a13 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 16 Sep 2024 12:06:20 -0400 Subject: [PATCH 255/393] Lingo: Added missing connection from The Tenacious -> Hub Room (#3947) --- worlds/lingo/data/LL1.yaml | 4 +++- worlds/lingo/data/generated.dat | Bin 149166 -> 149230 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 16a1573b1d56..bbed1464530b 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -482,7 +482,9 @@ Crossroads: door: Crossroads Entrance The Tenacious: - door: Tenacious Entrance + - door: Tenacious Entrance + - room: The Tenacious + door: Shortcut to Hub Room Near Far Area: True Hedge Maze: door: Shortcut to Hedge Maze diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index e2d3d06bec9642cf1b782cc3751ec542c322b558..789fc0856d62853f80fa5bd332b87c6fdd63ee60 100644 GIT binary patch delta 32275 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

U5jJVz{w%7y6n9Ni@HWmP7_!i>?bhpSWv&*7)( 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 delta 32247 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 UkY1CJx8$FF&puxoG}7^Z0FSwKuK)l5 From ce42e42af78676758f6bd30c1cceb576ebf1fcb6 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:36:05 -0500 Subject: [PATCH 256/393] Core: fix single player item links (#3721) * fix single player item links * Make a variable and fix weird spacing * use advancement instead of classification --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- BaseClasses.py | 2 ++ Fill.py | 20 +++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b40b872f0c8c..a5de1689a7fe 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -342,6 +342,8 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ region = Region("Menu", group_id, self, "ItemLink") self.regions.append(region) locations = region.locations + # ensure that progression items are linked first, then non-progression + self.itempool.sort(key=lambda item: item.advancement) for item in self.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: diff --git a/Fill.py b/Fill.py index e2fcff00358e..706cca657457 100644 --- a/Fill.py +++ b/Fill.py @@ -475,28 +475,26 @@ def mark_for_locking(location: Location): nonlocal lock_later lock_later.append(location) + single_player = multiworld.players == 1 and not multiworld.groups + if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking, - name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority") accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: # "advancement/progression fill" if panic_method == "swap": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, + name="Progression", single_player_placement=single_player) elif panic_method == "raise": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + name="Progression", single_player_placement=single_player) elif panic_method == "start_inventory": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, allow_partial=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + allow_partial=True, name="Progression", single_player_placement=single_player) if progitempool: for item in progitempool: logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") From b8d23ec5956cbf8313c328e4f3f9f9d08c9e0492 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 17 Sep 2024 13:41:56 +0100 Subject: [PATCH 257/393] MMBN3: Add missing indirect conditions (#3931) Entrances to SciLab_Cyberworld and Yoka_Cyberworld had logic for being able to reach SciLab_Overworld, but did not register this indirect condition. Entrances to Beach_Cyberworld had logic for being able to reach Yoka_Overworld, but did not register this indirect condition. Entrances to Undernet and Secret_Area had logic for having a high enough explore score, but explore score is calculated based on the accessibility of a number of regions and no indirect conditions were being registered for these regions. --- worlds/mmbn3/__init__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index 97725e728bae..6d28b101c377 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -97,6 +97,28 @@ def create_regions(self) -> None: add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) + + # Regions which contribute to explore score when accessible. + explore_score_region_names = ( + RegionName.WWW_Island, + RegionName.SciLab_Overworld, + RegionName.SciLab_Cyberworld, + RegionName.Yoka_Overworld, + RegionName.Yoka_Cyberworld, + RegionName.Beach_Overworld, + RegionName.Beach_Cyberworld, + RegionName.Undernet, + RegionName.Deep_Undernet, + RegionName.Secret_Area, + ) + explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names] + + # Entrances which use explore score in their logic need to register all the explore score regions as indirect + # conditions. + def register_explore_score_indirect_conditions(entrance): + for explore_score_region in explore_score_regions: + self.multiworld.register_indirect_condition(explore_score_region, entrance) + for region_info in regions: region = name_to_region[region_info.name] for connection in region_info.connections: @@ -119,6 +141,7 @@ def create_regions(self) -> None: entrance.access_rule = lambda state: \ state.has(ItemName.CSciPas, self.player) or \ state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Yoka_Cyberworld: entrance.access_rule = lambda state: \ state.has(ItemName.CYokaPas, self.player) or \ @@ -126,16 +149,19 @@ def create_regions(self) -> None: state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and state.has(ItemName.Press, self.player) ) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Beach_Cyberworld: entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) - + self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance) if connection == RegionName.Undernet: entrance.access_rule = lambda state: self.explore_score(state) > 8 and\ state.has(ItemName.Press, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.Secret_Area: entrance.access_rule = lambda state: self.explore_score(state) > 12 and\ state.has(ItemName.Hammer, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.WWW_Island: entrance.access_rule = lambda state:\ state.has(ItemName.Progressive_Undernet_Rank, self.player, 8) From 4692e6f08aa9c7cea764be0f079e506e17c695b0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:42:19 -0500 Subject: [PATCH 258/393] MM2: fix Air Shooter minimum damage #3922 --- worlds/mm2/rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index c30688f2adbe..eddd09927445 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -37,7 +37,7 @@ minimum_weakness_requirement: Dict[int, int] = { 0: 1, # Mega Buster is free 1: 14, # 2 shots of Atomic Fire - 2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot + 2: 2, # 14 shots of Air Shooter 3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off 4: 1, # 56 uses of Bubble Lead 5: 1, # 224 uses of Quick Boomerang From 1c0cec0de2311f818c1a19b4f0d91219e0d9c852 Mon Sep 17 00:00:00 2001 From: digiholic Date: Tue, 17 Sep 2024 06:42:48 -0600 Subject: [PATCH 259/393] [OSRS] Adds Description to OSRS World #3921 --- worlds/osrs/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 1b7ca9c1e0f4..49aa1666084e 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -33,6 +33,12 @@ class OSRSWeb(WebWorld): class OSRSWorld(World): + """ + The best retro fantasy MMORPG on the planet. Old School is RuneScape but… older! This is the open world you know and love, but as it was in 2007. + The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating + the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild! + """ + game = "Old School Runescape" options_dataclass = OSRSOptions options: OSRSOptions @@ -635,7 +641,7 @@ def can_gold(state): else: return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "Cooking": + if skill.lower() == "cooking": if self.options.brutal_grinds or level < 15: return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ state.can_reach(RegionNames.Egg, "Region", self.player) or \ From f8d3c26e3c6e4972f7845c6cd10bede41d8fd7cf Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 05:43:22 -0700 Subject: [PATCH 260/393] Pokemon Emerald: Fix unguarded wonder trade write (#3939) --- worlds/pokemon_emerald/client.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index cda829def9d9..d742b8936f14 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -133,6 +133,7 @@ class PokemonEmeraldClient(BizHawkClient): latest_wonder_trade_reply: dict wonder_trade_cooldown: int wonder_trade_cooldown_timer: int + queued_received_trade: Optional[str] death_counter: Optional[int] previous_death_link: float @@ -153,6 +154,7 @@ def initialize_client(self): self.previous_death_link = 0 self.ignore_next_death_link = False self.current_map = None + self.queued_received_trade = None async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: from CommonClient import logger @@ -548,22 +550,29 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st (sb1_address + 0x37CC, [1], "System Bus"), ]) elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2: - # Game is waiting on receiving a trade. See if there are any available trades that were not - # sent by this player, and if so, try to receive one. - if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # Game is waiting on receiving a trade. + if self.queued_received_trade is not None: + # Client is holding a trade, ready to write it into the game + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ + (sb1_address + 0x377C, json_to_pokemon_data(self.queued_received_trade), "System Bus"), + ], [guards["SAVE BLOCK 1"]]) + + # Notify the player if it was written, otherwise hold it for the next loop + if success: + logger.info("Wonder trade received!") + self.queued_received_trade = None + + elif self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # See if there are any available trades that were not sent by this player. If so, try to receive one. if any(item[0] != ctx.slot for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items() if key != "_lock" and orjson.loads(item[1])["species"] <= 386): - received_trade = await self.wonder_trade_receive(ctx) - if received_trade is None: + self.queued_received_trade = await self.wonder_trade_receive(ctx) + if self.queued_received_trade is None: self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown self.wonder_trade_cooldown *= 2 self.wonder_trade_cooldown += random.randrange(0, 500) else: - await bizhawk.write(ctx.bizhawk_ctx, [ - (sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"), - ]) - logger.info("Wonder trade received!") self.wonder_trade_cooldown = 5000 else: From ec50b0716aa280c7bf4ce7a13691de8dc95f8b34 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 17 Sep 2024 07:44:32 -0500 Subject: [PATCH 261/393] Core: Add color conversions for colorama/terminal output #3940 --- NetUtils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NetUtils.py b/NetUtils.py index c451fa3f8460..4776b228db17 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -273,7 +273,8 @@ def _handle_color(self, node: JSONMessagePart): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47, + 'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors def color_code(*args): From 96542fb2d891ff26c86b8a652907980ea3686702 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:08:15 -0400 Subject: [PATCH 262/393] Blasphemous: Move pre_fill to create_items #3901 --- worlds/blasphemous/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index b110c316da48..67031710e4eb 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -199,8 +199,6 @@ def create_items(self): self.multiworld.itempool += pool - - def pre_fill(self): self.place_items_from_dict(unrandomized_dict) if self.options.thorn_shuffle == "vanilla": @@ -335,4 +333,4 @@ class BlasphemousItem(Item): class BlasphemousLocation(Location): - game: str = "Blasphemous" \ No newline at end of file + game: str = "Blasphemous" From dae3fe188d253bfd8340a15ff5b44a8189413008 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 17 Sep 2024 14:11:35 +0100 Subject: [PATCH 263/393] OOT: Fix incorrect region accessibility after update_reachable_regions() (#3712) `CollectionState.update_reachable_regions()` un-stales the state for all players, but when checking `OOTRegion.can_reach()`, it would only update OOT's age region accessibility when the state was stale, so if the state was always un-staled by `update_reachable_regions()` immediately before `OOTRegion.can_reach()`, OOT's age region accessibility would never update. This patch fixes the issue by replacing use of CollectionState.stale with a separate stale state dictionary specific to OOT that is only un-staled by `_oot_update_age_reachable_regions()`. OOT's collect() and remove() implementations have been updated to stale the new OOT-specific state. --- worlds/oot/Regions.py | 2 +- worlds/oot/Rules.py | 13 +++++++++---- worlds/oot/__init__.py | 9 +++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/worlds/oot/Regions.py b/worlds/oot/Regions.py index 5d5cc9b13822..4a3d7e416a15 100644 --- a/worlds/oot/Regions.py +++ b/worlds/oot/Regions.py @@ -64,7 +64,7 @@ def get_scene(self): return None def can_reach(self, state): - if state.stale[self.player]: + if state._oot_stale[self.player]: stored_age = state.age[self.player] state._oot_update_age_reachable_regions(self.player) state.age[self.player] = stored_age diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 4bbf15435cfe..36563a3f9f27 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -8,12 +8,17 @@ from .Items import oot_is_item_of_type from .LocationList import dungeon_song_locations -from BaseClasses import CollectionState +from BaseClasses import CollectionState, MultiWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item from ..AutoWorld import LogicMixin class OOTLogic(LogicMixin): + def init_mixin(self, parent: MultiWorld): + # Separate stale state for OOTRegion.can_reach() to use because CollectionState.update_reachable_regions() sets + # `self.state[player] = False` for all players without updating OOT's age region accessibility. + self._oot_stale = {player: True for player, world in parent.worlds.items() + if parent.worlds[player].game == "Ocarina of Time"} def _oot_has_stones(self, count, player): return self.has_group("stones", player, count) @@ -92,9 +97,9 @@ def _oot_reach_at_time(self, regionname, tod, already_checked, player): return False # Store the age before calling this! - def _oot_update_age_reachable_regions(self, player): - self.stale[player] = False - for age in ['child', 'adult']: + def _oot_update_age_reachable_regions(self, player): + self._oot_stale[player] = False + for age in ['child', 'adult']: self.age[player] = age rrp = getattr(self, f'{age}_reachable_regions')[player] bc = getattr(self, f'{age}_blocked_connections')[player] diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index ee78958b2dbe..94587a41a0f2 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1301,6 +1301,7 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: # the appropriate number of keys in the collection state when they are # picked up. def collect(self, state: CollectionState, item: OOTItem) -> bool: + state._oot_stale[self.player] = True if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') state.prog_items[self.player][alt_item_name] += count @@ -1313,8 +1314,12 @@ def remove(self, state: CollectionState, item: OOTItem) -> bool: state.prog_items[self.player][alt_item_name] -= count if state.prog_items[self.player][alt_item_name] < 1: del (state.prog_items[self.player][alt_item_name]) + state._oot_stale[self.player] = True return True - return super().remove(state, item) + changed = super().remove(state, item) + if changed: + state._oot_stale[self.player] = True + return changed # Helper functions @@ -1389,7 +1394,7 @@ def get_state_with_complete_itempool(self): # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True) - all_state.stale[self.player] = True + all_state._oot_stale[self.player] = True return all_state From 97be5f1dde63e4fbec51f8973a184a7c66dd6a37 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:13:19 +0200 Subject: [PATCH 264/393] YGO06: slotdata fix (#3953) * YGO06: fix slot data for universal tracker * YGO06: put Extremely Low Deck Bonus after Low Deck Bonus --- worlds/yugioh06/__init__.py | 2 +- worlds/yugioh06/rules.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 1cf44f090fed..a39b52cd09d5 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -430,7 +430,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "final_campaign_boss_campaign_opponents": self.options.final_campaign_boss_campaign_opponents.value, "fourth_tier_5_campaign_boss_campaign_opponents": - self.options.fourth_tier_5_campaign_boss_unlock_condition.value, + self.options.fourth_tier_5_campaign_boss_campaign_opponents.value, "third_tier_5_campaign_boss_campaign_opponents": self.options.third_tier_5_campaign_boss_campaign_opponents.value, "number_of_challenges": self.options.number_of_challenges.value, diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py index a804c7e7286a..0b46e0b5d0b0 100644 --- a/worlds/yugioh06/rules.py +++ b/worlds/yugioh06/rules.py @@ -39,10 +39,10 @@ def set_rules(world): "No Trap Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2), "No Damage Bonus": lambda state: state.has_group("Campaign Boss Beaten", player, 3), "Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 3), + yugioh06_difficulty(state, player, 2), "Extremely Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 2), + yugioh06_difficulty(state, player, 3), "Opponent's Turn Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Exactly 0 LP Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Reversal Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), From 5aea8d4ab56fcb063ce672d44bcf1d97229d705d Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 06:14:05 -0700 Subject: [PATCH 265/393] Pokemon Emerald: Update changelog (#3952) --- worlds/pokemon_emerald/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0437c0dae8ff..6a1844e79fde 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,21 @@ +# 2.3.0 + +### Features + +- Added a Swedish translation of the setup guide. +- The client communicates map transitions to any trackers connected to the slot. +- Added the player's Normalize Encounter Rates option to slot data for trackers. + +### Fixes + +- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if +the player randomized NPC gifts. +- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. +- A Team Magma Grunt in the Space Center which could become unreachable while trainersanity is active by overlapping +with another NPC was moved to an unoccupied space. +- Fixed a problem where the client would crash on certain operating systems while using certain python versions if the +player tried to wonder trade. + # 2.2.0 ### Features @@ -175,6 +193,7 @@ turn to face you when you run. species equally likely to appear, but makes rare encounters less rare. - Added `Trick House` location group. - Removed `Postgame Locations` location group. +- Added a Spanish translation of the setup guide. ### QoL From 8f7e0dc441610e292cfb1ce5688a2017fe175ae3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 14:17:41 -0700 Subject: [PATCH 266/393] Core: Improve death link option description (#3951) --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index b79714635d9e..ac4b2b8cd8bb 100644 --- a/Options.py +++ b/Options.py @@ -1335,7 +1335,7 @@ class PriorityLocations(LocationSet): class DeathLink(Toggle): - """When you die, everyone dies. Of course the reverse is true too.""" + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" display_name = "Death Link" rich_text_doc = True From b982e9ebb4eb3a370efb36860b8e51a80881d24a Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Tue, 17 Sep 2024 23:18:43 +0200 Subject: [PATCH 267/393] SC2: Fix /received display bugs (#3949) * SC2: Fix location display in /received command * SC2: Backport broken markup fix in /received output from the dev branch * Cleanup --- worlds/sc2/Client.py | 14 +++++++------- worlds/sc2/ClientGui.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index bb325ba1da45..813cf2884517 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -97,12 +97,12 @@ class ConfigurableOptionInfo(typing.NamedTuple): class ColouredMessage: - def __init__(self, text: str = '') -> None: + def __init__(self, text: str = '', *, keep_markup: bool = False) -> None: self.parts: typing.List[dict] = [] if text: - self(text) - def __call__(self, text: str) -> 'ColouredMessage': - add_json_text(self.parts, text) + self(text, keep_markup=keep_markup) + def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage': + add_json_text(self.parts, text, keep_markup=keep_markup) return self def coloured(self, text: str, colour: str) -> 'ColouredMessage': add_json_text(self.parts, text, type="color", color=colour) @@ -128,7 +128,7 @@ def formatted_print(self, text: str) -> None: # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output. # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs, # without having to branch code from CommonClient - self.ctx.on_print_json({"data": [{"text": text}]}) + self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]}) def _cmd_difficulty(self, difficulty: str = "") -> bool: """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" @@ -257,7 +257,7 @@ def print_faction_title(): print_faction_title() has_printed_faction_title = True (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) @@ -278,7 +278,7 @@ def print_faction_title(): for item in received_items_of_this_type: filter_match_count += len(received_items_of_this_type) (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 22e444efe7c9..fe62e6162457 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -1,7 +1,8 @@ from typing import * import asyncio -from kvui import GameManager, HoverBehavior, ServerToolTip +from NetUtils import JSONMessagePart +from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -69,6 +70,18 @@ class MissionLayout(GridLayout): class MissionCategory(GridLayout): pass + +class SC2JSONtoKivyParser(KivyJSONtoTextParser): + def _handle_text(self, node: JSONMessagePart): + if node.get("keep_markup", False): + for ref in node.get("refs", []): + node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" + self.ref_count += 1 + return super(KivyJSONtoTextParser, self)._handle_text(node) + else: + return super()._handle_text(node) + + class SC2Manager(GameManager): logging_pairs = [ ("Client", "Archipelago"), @@ -87,6 +100,7 @@ class SC2Manager(GameManager): def __init__(self, ctx) -> None: super().__init__(ctx) + self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) def clear_tooltip(self) -> None: if self.ctx.current_tooltip: From d1a7bc66e6f217d9c024e99b8556c2e981ab208a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 17 Sep 2024 14:49:36 -0700 Subject: [PATCH 268/393] Pokemon Emerald: Prevent client from spamming goal status update (#3900) --- worlds/pokemon_emerald/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index d742b8936f14..c91b7d3e26b0 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -352,6 +352,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # Send game clear if not ctx.finished_game and game_clear: + ctx.finished_game = True await ctx.send_msgs([{ "cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL, From 78c5489189596d3f3851a119efebf58e94cea788 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 17 Sep 2024 21:50:02 +0000 Subject: [PATCH 269/393] DS3: Mark the Archdeacon Set as downstream of Deacons of the Deep (#3883) This ensures that if Deacons is replaced with Yhorm, the Storm Ruler won't show up in these locations. --- worlds/dark_souls_3/Bosses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py index 008a29713202..fac7d913c338 100644 --- a/worlds/dark_souls_3/Bosses.py +++ b/worlds/dark_souls_3/Bosses.py @@ -63,6 +63,9 @@ class DS3BossInfo: DS3BossInfo("Deacons of the Deep", 3500800, locations = { "CD: Soul of the Deacons of the Deep", "CD: Small Doll - boss drop", + "CD: Archdeacon White Crown - boss room after killing boss", + "CD: Archdeacon Holy Garb - boss room after killing boss", + "CD: Archdeacon Skirt - boss room after killing boss", "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", }), DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { From dc218b79974f5d0418b5d2e200106519256751a0 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:56:40 +0200 Subject: [PATCH 270/393] LADX: Adding Slot Data For Magpie Tracker (#3582) * wip: LADX slot_data * LADX: slot_data * Sending slot_data to magpie. * Moved sending slot_data from pushing to pull by Magpie request. * Adding EoF newline to tracker.py. * Update Tracker.py * Update __init__.py * Update LinksAwakeningClient.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- LinksAwakeningClient.py | 5 +++++ worlds/ladx/Tracker.py | 18 +++++++++++++++++- worlds/ladx/__init__.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index a51645feac92..298788098d9e 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: self.client = LinksAwakeningClient() + self.slot_data = {} + if magpie: self.magpie_enabled = True self.magpie = MagpieBridge() @@ -564,6 +566,8 @@ async def server_auth(self, password_requested: bool = False): def on_package(self, cmd: str, args: dict): if cmd == "Connected": self.game = self.slot_info[self.slot].game + self.slot_data = args.get("slot_data", {}) + # TODO - use watcher_event if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): @@ -628,6 +632,7 @@ async def deathlink(): self.magpie.set_checks(self.client.tracker.all_checks) await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.send_gps(self.client.gps_tracker) + self.magpie.slot_data = self.slot_data except Exception: # Don't let magpie errors take out the client pass diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index 851fca164453..5f48b64c4f5e 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -149,6 +149,8 @@ class MagpieBridge: item_tracker = None ws = None features = [] + slot_data = {} + async def handler(self, websocket): self.ws = websocket while True: @@ -163,6 +165,9 @@ async def handler(self, websocket): await self.send_all_inventory() if "checks" in self.features: await self.send_all_checks() + if "slot_data" in self.features: + await self.send_slot_data(self.slot_data) + # Translate renamed IDs back to LADXR IDs @staticmethod def fixup_id(the_id): @@ -222,6 +227,18 @@ async def send_gps(self, gps): return await gps.send_location(self.ws) + async def send_slot_data(self, slot_data): + if not self.ws: + return + + logger.debug("Sending slot_data to magpie.") + message = { + "type": "slot_data", + "slot_data": slot_data + } + + await self.ws.send(json.dumps(message)) + async def serve(self): async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): await asyncio.Future() # run forever @@ -237,4 +254,3 @@ async def set_item_tracker(self, item_tracker): await self.send_all_inventory() else: await self.send_inventory_diffs() - diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index c958ef212fe4..79f1fe470f81 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -512,3 +512,31 @@ def remove(self, state, item: Item) -> bool: if change and item.name in self.rupees: state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change + + def fill_slot_data(self): + slot_data = {} + + if not self.multiworld.is_race: + # all of these option are NOT used by the LADX- or Text-Client. + # they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API) + # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly + + slot_options = ["instrument_count"] + + slot_options_display_name = [ + "goal", "logic", "tradequest", "rooster", + "experimental_dungeon_shuffle", "experimental_entrance_shuffle", "trendy_game", "gfxmod", + "shuffle_nightmare_keys", "shuffle_small_keys", "shuffle_maps", + "shuffle_compasses", "shuffle_stone_beaks", "shuffle_instruments", "nag_messages" + ] + + # use the default behaviour to grab options + slot_data = self.options.as_dict(*slot_options) + + # for options which should not get the internal int value but the display name use the extra handling + slot_data.update({ + option: value.current_key + for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name + }) + + return slot_data From 4ea1dddd2f420bca6a73e13d04228931f04a3834 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 17 Sep 2024 17:57:55 -0400 Subject: [PATCH 271/393] TUNIC: Better logic for Library Lab glass and Fortress leaf piles #3880 --- worlds/tunic/er_rules.py | 15 ++++++++++++++- worlds/tunic/rules.py | 13 ++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 65175e41ca14..ee48f60eaca4 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1339,13 +1339,26 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player)) + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee + set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), + lambda state: has_stick(state, player) or state.has(ice_dagger, player)) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), - lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) # Quarry set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 942bbc773aa5..14ed84d44964 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -296,9 +296,20 @@ def set_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), - lambda state: state.has(laurels, player)) + lambda state: state.has(laurels, player) and (has_stick(state, player) or state.has(ice_dagger, player))) set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player) and (has_ability(prayer, state, world) From 30a0b337a2bc79407127b6b60a86c4c1793bc5be Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:58:45 -0400 Subject: [PATCH 272/393] DS3: Make Red Eye Orb always require Lift Chamber Key #3857 --- worlds/dark_souls_3/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 46c7ef1336c1..b51668539be2 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -612,9 +612,7 @@ def set_rules(self) -> None: self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") # Define the access rules to some specific locations - if self._is_location_available("FS: Lift Chamber Key - Leonhard"): - self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", - "Lift Chamber Key") + self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key") self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", "Jailbreaker's Key") self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") From 4e60f3cc54063051393cdd7bd253464398a08146 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 17 Sep 2024 17:00:26 -0500 Subject: [PATCH 273/393] The Messenger: Fix Portal Plando Issues (#3838) * add a more clear error message for a missing exit * remove portal region from the available pool * ensure plando portals are in the correct spot in the list and it gets cleared correctly --- worlds/messenger/portals.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 1da210cb23ff..17152a1a1538 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -215,13 +215,13 @@ def create_mapping(in_portal: str, warp: str) -> str: if "Portal" in warp: exit_string += "Portal" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}00")) elif warp in SHOP_POINTS[parent]: exit_string += f"{warp} Shop" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: exit_string += f"{warp} Checkpoint" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) @@ -230,12 +230,15 @@ def create_mapping(in_portal: str, warp: str) -> str: def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" + nonlocal available_portals + for connection in plando_connections: - if connection.entrance not in PORTALS: - continue # let it crash here if input is invalid - create_mapping(connection.entrance, connection.exit) + available_portals.remove(connection.exit) + parent = create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) + if shuffle_type < ShufflePortals.option_anywhere: + available_portals = [port for port in available_portals if port not in shop_points[parent]] shuffle_type = world.options.shuffle_portals shop_points = deepcopy(SHOP_POINTS) @@ -251,8 +254,13 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: plando = world.options.portal_plando.value if not plando: plando = world.options.plando_connections.value - if plando and world.multiworld.plando_options & PlandoOptions.connections: - handle_planned_portals(plando) + if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals: + try: + handle_planned_portals(plando) + # any failure i expect will trigger on available_portals.remove + except ValueError: + raise ValueError(f"Unable to complete portal plando for Player {world.player_name}. " + f"If you attempted to plando a checkpoint, checkpoints must be shuffled.") for portal in PORTALS: if portal in world.plando_portals: @@ -276,8 +284,13 @@ def disconnect_portals(world: "MessengerWorld") -> None: entrance.connected_region = None if portal in world.spoiler_portal_mapping: del world.spoiler_portal_mapping[portal] - if len(world.portal_mapping) > len(world.spoiler_portal_mapping): - world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)] + if world.plando_portals: + indexes = [PORTALS.index(portal) for portal in world.plando_portals] + planned_portals = [] + for index, portal_coord in enumerate(world.portal_mapping): + if index in indexes: + planned_portals.append(portal_coord) + world.portal_mapping = planned_portals def validate_portals(world: "MessengerWorld") -> bool: From a7c96436d9b8f51b98533141815fc4658cbb256a Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 18 Sep 2024 01:03:33 +0300 Subject: [PATCH 274/393] Stardew valley: Add Marlon bedroom entrance rule (#3735) * - Created a test for the "Mapping Cave Systems" book * - Added missing rule to marlon's bedroom * - Can kill any monster, not just green slime * - Added a compound source structure, but I ended up deciding to not use it here. Still keeping it as it will probably be useful eventually * - Use the compound source of the monster compoundium (ironic, I know) * - Add required elevators --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- .../content/vanilla/pelican_town.py | 8 +++--- worlds/stardew_valley/data/game_item.py | 5 ++++ worlds/stardew_valley/logic/source_logic.py | 12 +++++++-- worlds/stardew_valley/rules.py | 2 ++ worlds/stardew_valley/test/__init__.py | 4 +-- worlds/stardew_valley/test/rules/TestBooks.py | 26 +++++++++++++++++++ 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 worlds/stardew_valley/test/rules/TestBooks.py diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 220b46eae2a4..73cc8f119a3e 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -1,6 +1,6 @@ from ..game_content import ContentPack from ...data import villagers_data, fish_data -from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource @@ -229,8 +229,10 @@ 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),), + CompoundSource(sources=( + 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)), diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 2107ca30d33a..6c8d30ed8e6f 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -59,6 +59,11 @@ class CustomRuleSource(ItemSource): create_rule: Callable[[Any], StardewRule] +@dataclass(frozen=True, **kw_only) +class CompoundSource(ItemSource): + sources: Tuple[ItemSource, ...] = () + + class Tag(ItemSource): """Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking.""" tag: Tuple[ItemTag, ...] diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py index 0e9b8e976f5b..9ef68a020eef 100644 --- a/worlds/stardew_valley/logic/source_logic.py +++ b/worlds/stardew_valley/logic/source_logic.py @@ -12,7 +12,7 @@ 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.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, -ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): def has_access_to_item(self, item: GameItem): rules = [] @@ -40,6 +40,10 @@ 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)) + def has_access_to_all(self, sources: Iterable[ItemSource]): + return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) + for source in sources)) + @functools.singledispatchmethod def has_access_to(self, source: Any): raise ValueError(f"Sources of type{type(source)} have no rule registered.") @@ -52,6 +56,10 @@ def _(self, source: GenericSource): def _(self, source: CustomRuleSource): return source.create_rule(self.logic) + @has_access_to.register + def _(self, source: CompoundSource): + return self.logic.source.has_access_to_all(source.sources) + @has_access_to.register def _(self, source: ForagingSource): return self.logic.harvesting.can_forage_from(source) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index e9bdd8c25bbb..7f39ee1ac2d4 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -39,6 +39,7 @@ from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance from .strings.forageable_names import Forageable +from .strings.generic_names import Generic from .strings.geode_names import Geode from .strings.material_names import Material from .strings.metal_names import MetalBar, Mineral @@ -263,6 +264,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S 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) + set_entrance_rule(multiworld, player, Entrance.adventurer_guild_to_bedroom, logic.monster.can_kill_max(Generic.any)) def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index e7278cba2800..3fe05d205ce0 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -256,10 +256,10 @@ def run_default_tests(self) -> bool: return False return super().run_default_tests - def collect_lots_of_money(self): + def collect_lots_of_money(self, percent: float = 0.25): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items - required_prog_items = int(round(real_total_prog_items * 0.25)) + required_prog_items = int(round(real_total_prog_items * percent)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items diff --git a/worlds/stardew_valley/test/rules/TestBooks.py b/worlds/stardew_valley/test/rules/TestBooks.py new file mode 100644 index 000000000000..6605e7e645e3 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBooks.py @@ -0,0 +1,26 @@ +from ... import options +from ...test import SVTestBase + + +class TestBooksLogic(SVTestBase): + options = { + options.Booksanity.internal_name: options.Booksanity.option_all, + } + + def test_need_weapon_for_mapping_cave_systems(self): + self.collect_lots_of_money(0.5) + + location = self.multiworld.get_location("Read Mapping Cave Systems", self.player) + + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Weapon") + self.assert_reach_location_true(location, self.multiworld.state) + + From 8c5b65ff26ef7042e94ae7cf211a1e41fdbd31ee Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 18 Sep 2024 01:07:40 +0300 Subject: [PATCH 275/393] Stardew Valley: Remove Accessibility and progression balancing from presets #3833 --- worlds/stardew_valley/presets.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index cf6f87a1501c..1861a914235c 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -57,8 +57,6 @@ } easy_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "very rich", @@ -103,8 +101,6 @@ } medium_settings = { - "progression_balancing": 25, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "rich", @@ -149,8 +145,6 @@ } hard_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_grandpa_evaluation, FarmType.internal_name: "random", StartingMoney.internal_name: "extra", @@ -195,8 +189,6 @@ } nightmare_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "vanilla", @@ -241,8 +233,6 @@ } short_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_bottom_of_the_mines, FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", @@ -287,8 +277,6 @@ } minsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_minimal, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, @@ -333,8 +321,6 @@ } allsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, From debb93661803dd2b114ee1975ef4dca74d7528df Mon Sep 17 00:00:00 2001 From: sgrunt Date: Tue, 17 Sep 2024 16:08:18 -0600 Subject: [PATCH 276/393] DOOM II: Fix sector 95 assignment in DOOM II MAP17 to correctly flag the BFG9000 location as in the Yellow Key area (#3705) Co-authored-by: sgrunt --- worlds/doom_ii/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py index 3ce87b8a6662..376f19446f21 100644 --- a/worlds/doom_ii/Locations.py +++ b/worlds/doom_ii/Locations.py @@ -1470,7 +1470,7 @@ class LocationDict(TypedDict, total=False): 'map': 6, 'index': 102, 'doom_type': 2006, - 'region': "Tenements (MAP17) Main"}, + 'region': "Tenements (MAP17) Yellow"}, 361243: {'name': 'Tenements (MAP17) - Plasma gun', 'episode': 2, 'map': 6, From 6fac83b84cab8909e8104c931781ffb9d18945b4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 18 Sep 2024 00:18:17 +0200 Subject: [PATCH 277/393] Factorio: update API use (#3760) --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/factorio/Mod.py | 39 ++++----- worlds/factorio/Options.py | 90 +++++++++----------- worlds/factorio/Shapes.py | 10 +-- worlds/factorio/Technologies.py | 5 +- worlds/factorio/__init__.py | 141 ++++++++++++++++---------------- 5 files changed, 136 insertions(+), 149 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index d7b3d4b1ebca..7eec71875829 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,5 +1,6 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" +import dataclasses import json import os import shutil @@ -88,6 +89,8 @@ def write_contents(self, opened_zipfile: zipfile.ZipFile): def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.multiworld + random = world.random + global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: @@ -110,8 +113,6 @@ def load_template(name: str): mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" versioned_mod_name = mod_name + "_" + Utils.__version__ - random = multiworld.per_slot_randoms[player] - def flop_random(low, high, base=None): """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" if base: @@ -129,43 +130,43 @@ def flop_random(low, high, base=None): "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, - "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "custom_technologies": multiworld.worlds[player].custom_technologies, + "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), + "custom_technologies": world.custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, - "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, + "slot_name": world.player_name, "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": multiworld.starting_items[player], "recipes": recipes, + "starting_items": world.options.starting_items, "recipes": recipes, "random": random, "flop_random": flop_random, - "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), - "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), + "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), + "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, "progressive_technology_table": {tech.name: tech.progressive for tech in progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, - "max_science_pack": multiworld.max_science_pack[player].value, + "max_science_pack": world.options.max_science_pack.value, "liquids": fluids, - "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value, + "goal": world.options.goal.value, + "energy_link": world.options.energy_link.value, "useless_technologies": useless_technologies, - "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, + "chunk_shuffle": 0, } - for factorio_option in Options.factorio_options: + for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items(): if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: continue - template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value + template_data[factorio_option] = factorio_option_instance.value - if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: + if world.options.silo == Options.Silo.option_randomize_recipe: template_data["free_sample_blacklist"]["rocket-silo"] = 1 - if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: + if world.options.satellite == Options.Satellite.option_randomize_recipe: template_data["free_sample_blacklist"]["satellite"] = 1 - template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) - template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) + template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value}) + template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value}) zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + mod = FactorioModFile(zf_path, player=player, player_name=world.player_name) if world.zip_path: with zipfile.ZipFile(world.zip_path) as zf: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 3429ebbd4251..788d1f9e1d92 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,11 +1,14 @@ from __future__ import annotations -import typing + +from dataclasses import dataclass import datetime +import typing -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ - StartInventoryPool from schema import Schema, Optional, And, Or +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool, PerGameCommonOptions + # schema helpers FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) @@ -422,50 +425,37 @@ class EnergyLink(Toggle): display_name = "EnergyLink" -factorio_options: typing.Dict[str, type(Option)] = { - "max_science_pack": MaxSciencePack, - "goal": Goal, - "tech_tree_layout": TechTreeLayout, - "min_tech_cost": MinTechCost, - "max_tech_cost": MaxTechCost, - "tech_cost_distribution": TechCostDistribution, - "tech_cost_mix": TechCostMix, - "ramping_tech_costs": RampingTechCosts, - "silo": Silo, - "satellite": Satellite, - "free_samples": FreeSamples, - "tech_tree_information": TechTreeInformation, - "starting_items": FactorioStartItems, - "free_sample_blacklist": FactorioFreeSampleBlacklist, - "free_sample_whitelist": FactorioFreeSampleWhitelist, - "recipe_time": RecipeTime, - "recipe_ingredients": RecipeIngredients, - "recipe_ingredients_offset": RecipeIngredientsOffset, - "imported_blueprints": ImportedBlueprint, - "world_gen": FactorioWorldGen, - "progressive": Progressive, - "teleport_traps": TeleportTrapCount, - "grenade_traps": GrenadeTrapCount, - "cluster_grenade_traps": ClusterGrenadeTrapCount, - "artillery_traps": ArtilleryTrapCount, - "atomic_rocket_traps": AtomicRocketTrapCount, - "attack_traps": AttackTrapCount, - "evolution_traps": EvolutionTrapCount, - "evolution_trap_increase": EvolutionTrapIncrease, - "death_link": DeathLink, - "energy_link": EnergyLink, - "start_inventory_from_pool": StartInventoryPool, -} - -# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. -if datetime.datetime.today().month == 4: - - class ChunkShuffle(Toggle): - """Entrance Randomizer.""" - display_name = "Chunk Shuffle" - - - if datetime.datetime.today().day > 1: - ChunkShuffle.__doc__ += """ - 2023 April Fool's option. Shuffles chunk border transitions.""" - factorio_options["chunk_shuffle"] = ChunkShuffle +@dataclass +class FactorioOptions(PerGameCommonOptions): + max_science_pack: MaxSciencePack + goal: Goal + tech_tree_layout: TechTreeLayout + min_tech_cost: MinTechCost + max_tech_cost: MaxTechCost + tech_cost_distribution: TechCostDistribution + tech_cost_mix: TechCostMix + ramping_tech_costs: RampingTechCosts + silo: Silo + satellite: Satellite + free_samples: FreeSamples + tech_tree_information: TechTreeInformation + starting_items: FactorioStartItems + free_sample_blacklist: FactorioFreeSampleBlacklist + free_sample_whitelist: FactorioFreeSampleWhitelist + recipe_time: RecipeTime + recipe_ingredients: RecipeIngredients + recipe_ingredients_offset: RecipeIngredientsOffset + imported_blueprints: ImportedBlueprint + world_gen: FactorioWorldGen + progressive: Progressive + teleport_traps: TeleportTrapCount + grenade_traps: GrenadeTrapCount + cluster_grenade_traps: ClusterGrenadeTrapCount + artillery_traps: ArtilleryTrapCount + atomic_rocket_traps: AtomicRocketTrapCount + attack_traps: AttackTrapCount + evolution_traps: EvolutionTrapCount + evolution_trap_increase: EvolutionTrapIncrease + death_link: DeathLink + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index d40871f7fa82..2a81cc3fb004 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"): return location.complexity, location.rel_cost -def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: - world = factorio_world.multiworld - player = factorio_world.player +def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} - layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) + layout = world.options.tech_tree_layout.value + locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: @@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - factorio_world.tech_tree_layout_prerequisites = prerequisites + world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 096396c0e774..112cc49f0920 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -13,12 +13,11 @@ from . import Options factorio_tech_id = factorio_base_id = 2 ** 17 -# Factorio technologies are imported from a .json document in /data -source_folder = os.path.join(os.path.dirname(__file__), "data") pool = ThreadPoolExecutor(1) +# Factorio technologies are imported from a .json document in /data def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json")) @@ -99,7 +98,7 @@ def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: i and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) or origin.name == "rocket-silo") self.player = player - if origin.name not in world.worlds[player].special_nodes: + if origin.name not in world.special_nodes: if military_allowed: ingredients.add("military-science-pack") ingredients = list(ingredients) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 753c567286e0..925327655a24 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -11,7 +11,7 @@ from worlds.generic import Rules from .Locations import location_pools, location_table from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution +from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ @@ -89,13 +89,15 @@ class Factorio(World): advancement_technologies: typing.Set[str] web = FactorioWeb() + options_dataclass = FactorioOptions + options: FactorioOptions item_name_to_id = all_items location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 4, 2) + required_client_version = (0, 5, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] @@ -117,32 +119,32 @@ def __init__(self, world, player: int): def generate_early(self) -> None: # if max < min, then swap max and min - if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: - self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ - self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value - self.tech_mix = self.multiworld.tech_cost_mix[self.player] - self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn + if self.options.max_tech_cost < self.options.min_tech_cost: + self.options.min_tech_cost.value, self.options.max_tech_cost.value = \ + self.options.max_tech_cost.value, self.options.min_tech_cost.value + self.tech_mix = self.options.tech_cost_mix.value + self.skip_silo = self.options.silo.value == Silo.option_spawn def create_regions(self): player = self.player - random = self.multiworld.random + random = self.random nauvis = Region("Nauvis", player, self.multiworld) location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player] + \ - self.multiworld.attack_traps[player] + \ - self.multiworld.teleport_traps[player] + \ - self.multiworld.grenade_traps[player] + \ - self.multiworld.cluster_grenade_traps[player] + \ - self.multiworld.atomic_rocket_traps[player] + \ - self.multiworld.artillery_traps[player] + self.options.evolution_traps + \ + self.options.attack_traps + \ + self.options.teleport_traps + \ + self.options.grenade_traps + \ + self.options.cluster_grenade_traps + \ + self.options.atomic_rocket_traps + \ + self.options.artillery_traps location_pool = [] - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): location_pool.extend(location_pools[pack]) try: - location_names = self.multiworld.random.sample(location_pool, location_count) + location_names = random.sample(location_pool, location_count) except ValueError as e: # should be "ValueError: Sample larger than population or is negative" raise Exception("Too many traps for too few locations. Either decrease the trap count, " @@ -150,9 +152,9 @@ def create_regions(self): self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] - distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] - min_cost = self.multiworld.min_tech_cost[self.player] - max_cost = self.multiworld.max_tech_cost[self.player] + distribution: TechCostDistribution = self.options.tech_cost_distribution + min_cost = self.options.min_tech_cost.value + max_cost = self.options.max_tech_cost.value if distribution == distribution.option_even: rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: @@ -161,7 +163,7 @@ def create_regions(self): distribution.option_high: max_cost}[distribution.value] rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) - if self.multiworld.ramping_tech_costs[self.player]: + if self.options.ramping_tech_costs: def sorter(loc: FactorioScienceLocation): return loc.complexity, loc.rel_cost else: @@ -176,7 +178,7 @@ def sorter(loc: FactorioScienceLocation): event = FactorioItem("Victory", ItemClassification.progression, None, player) location.place_locked_item(event) - for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()): location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) @@ -185,24 +187,23 @@ def sorter(loc: FactorioScienceLocation): self.multiworld.regions.append(nauvis) def create_items(self) -> None: - player = self.player self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in - range(getattr(self.multiworld, - f"{trap_name.lower().replace(' ', '_')}_traps")[player])) + range(getattr(self.options, + f"{trap_name.lower().replace(' ', '_')}_traps"))) - want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. - want_progressives(self.multiworld.random)) + want_progressives = collections.defaultdict(lambda: self.options.progressive. + want_progressives(self.random)) cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation - if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: + if self.options.tech_tree_information == TechTreeInformation.option_full: # mark all locations as pre-hinted for loc in self.science_locations: loc.revealed = True @@ -229,14 +230,13 @@ def create_items(self) -> None: loc.revealed = True def set_rules(self): - world = self.multiworld player = self.player shapes = get_shapes(self) - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) + for ingredient in self.options.max_science_pack.get_allowed_packs(): + location = self.get_location(f"Automate {ingredient}") - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: custom_recipe = self.custom_recipes[ingredient] location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ @@ -257,30 +257,30 @@ def set_rules(self): prerequisites: all(state.can_reach(loc) for loc in locations)) silo_recipe = None - if self.multiworld.silo[self.player] == Silo.option_spawn: + if self.options.silo == Silo.option_spawn: silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ else next(iter(all_product_sources.get("rocket-silo"))) part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None - if self.multiworld.goal[self.player] == Goal.option_satellite: + if self.options.goal == Goal.option_satellite: satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ else next(iter(all_product_sources.get("satellite"))) victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.multiworld.silo[self.player] != Silo.option_spawn: + if self.options.silo != Silo.option_spawn: victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) + self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) - world.completion_condition[player] = lambda state: state.has('Victory', player) + self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) def generate_basic(self): - map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] + map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 # 32 bit uint - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) + map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1) - start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value + start_location_hints: typing.Set[str] = self.options.start_location_hints.value for loc in self.science_locations: # show start_location_hints ingame @@ -304,8 +304,6 @@ def collect_item(self, state, item, remove=False): return super(Factorio, self).collect_item(state, item, remove) - option_definitions = factorio_options - @classmethod def stage_write_spoiler(cls, world, spoiler_handle): factorio_players = world.get_game_players(cls.game) @@ -345,7 +343,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: # have to first sort for determinism, while filtering out non-stacking items pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle - self.multiworld.random.shuffle(pool) + self.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor target_num_ingredients = len(original.ingredients) + ingredients_offset @@ -389,7 +387,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: if min_num > max_num: fallback_pool.append(ingredient) continue # can't use that ingredient - num = self.multiworld.random.randint(min_num, max_num) + num = self.random.randint(min_num, max_num) new_ingredients[ingredient] = num remaining_raw -= num * ingredient_raw remaining_energy -= num * ingredient_energy @@ -433,66 +431,66 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: def set_custom_technologies(self): custom_technologies = {} - allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs() + allowed_packs = self.options.max_science_pack.get_allowed_packs() for technology_name, technology in base_technology_table.items(): - custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player) + custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player) return custom_technologies def set_custom_recipes(self): - ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] + ingredients_offset = self.options.recipe_ingredients_offset original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) - self.multiworld.random.shuffle(valid_pool) + valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients) + self.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, original_rocket_part.products, original_rocket_part.energy)} - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: valid_pool = [] - for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs(): + for pack in self.options.max_science_pack.get_ordered_science_packs(): valid_pool += sorted(science_pack_pools[pack]) - self.multiworld.random.shuffle(valid_pool) + self.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= - ingredients_offset) + ingredients_offset.value) self.custom_recipes[pack] = new_recipe - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ - or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe \ + or self.options.satellite.value == Satellite.option_randomize_recipe: valid_pool = set() - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): valid_pool |= science_pack_pools[pack] - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["rocket-silo"] = new_recipe - if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.satellite.value == Satellite.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), - ingredients_offset=ingredients_offset) + sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset.value) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) + new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe - needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} - if self.multiworld.silo[self.player] != Silo.option_spawn: + needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} + if self.options.silo != Silo.option_spawn: needed_recipes |= {"rocket-silo"} - if self.multiworld.goal[self.player].value == Goal.option_satellite: + if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: @@ -542,7 +540,8 @@ def __init__(self, player: int, name: str, address: int, parent: Region): self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): - if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99): + if (parent.multiworld.worlds[self.player].options.tech_cost_mix > + parent.multiworld.worlds[self.player].random.randint(0, 99)): self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 @property From f73c0d9894e3ecbc9baec9c9a465a5f1cdd4fd18 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:47:26 +0200 Subject: [PATCH 278/393] WebHost: Better host room v2 (#3948) * WebHost: add spinner to room command and show error message if fetch fails due to NetworkError * WebHost: don't update room log while tab is inactive * WebHost: don't include log for automated requests * WebHost: refresh room also for re-spinups and do that from javascript * Test, WebHost: send fake user-agent where required * WebHost: remove wrong comment in host room --- WebHostLib/misc.py | 35 ++++-- WebHostLib/static/styles/hostRoom.css | 25 +++++ WebHostLib/templates/hostRoom.html | 151 +++++++++++++++++--------- test/webhost/test_host_room.py | 3 +- 4 files changed, 154 insertions(+), 60 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 01c1ad84a707..4784fcd9da63 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: return "Access Denied", 403 -@app.route('/room/', methods=['GET', 'POST']) +@app.post("/room/") +def host_room_command(room: UUID): + room: Room = Room.get(id=room) + if room is None: + return abort(404) + + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + return redirect(url_for("host_room", room=room.id)) + + +@app.get("/room/") def host_room(room: UUID): room: Room = Room.get(id=room) if room is None: return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - return redirect(url_for("host_room", room=room.id)) now = datetime.datetime.utcnow() # indicate that the page should reload to get the assigned port - should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) + should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) + or room.last_activity < now - datetime.timedelta(seconds=room.timeout)) with db_session: room.last_activity = now # will trigger a spinup, if it's not already running - def get_log(max_size: int = 1024000) -> str: + browser_tokens = "Mozilla", "Chrome", "Safari" + automated = ("update" in request.args + or "Discordbot" in request.user_agent.string + or not any(browser_token in request.user_agent.string for browser_token in browser_tokens)) + + def get_log(max_size: int = 0 if automated else 1024000) -> str: + if max_size == 0: + return "…" try: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: raw_size = 0 diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 827f74c04df7..625b78cc5d3f 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -58,3 +58,28 @@ overflow-y: auto; max-height: 400px; } + +.loader{ + display: inline-block; + visibility: hidden; + margin-left: 5px; + width: 40px; + aspect-ratio: 4; + --_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0); + background: + var(--_g) 0 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 100%; + animation: l7 1s infinite linear; +} + +.loader.loading{ + visibility: visible; +} + +@keyframes l7{ + 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} + 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%} + 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 } +} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index fa8e26c2cbf8..8e76dafc12fa 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -19,28 +19,30 @@ {% block body %} {% include 'header/grassHeader.html' %}

- {% if room.owner == session["_id"] %} - Room created from Seed #{{ room.seed.id|suuid }} -
- {% endif %} - {% if room.tracker %} - This room has a Multiworld Tracker - and a Sphere Tracker enabled. -
- {% endif %} - The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. - Should you wish to continue later, - anyone can simply refresh this page and the server will resume.
- {% if room.last_port == -1 %} - There was an error hosting this Room. Another attempt will be made on refreshing this page. - The most likely failure reason is that the multiworld is too old to be loaded now. - {% elif room.last_port %} - You can connect to this room by using - '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' - - in the client.
- {% endif %} + + {% if room.owner == session["_id"] %} + Room created from Seed #{{ room.seed.id|suuid }} +
+ {% endif %} + {% if room.tracker %} + This room has a Multiworld Tracker + and a Sphere Tracker enabled. +
+ {% endif %} + The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
+ {% if room.last_port == -1 %} + There was an error hosting this Room. Another attempt will be made on refreshing this page. + The most likely failure reason is that the multiworld is too old to be loaded now. + {% elif room.last_port %} + You can connect to this room by using + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' + + in the client.
+ {% endif %} +
{{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %}
@@ -49,6 +51,7 @@ +
@@ -62,6 +65,7 @@ let url = '{{ url_for('display_log', room = room.id) }}'; let bytesReceived = {{ log_len }}; let updateLogTimeout; + let updateLogImmediately = false; let awaitingCommandResponse = false; let logger = document.getElementById("logger"); @@ -78,29 +82,36 @@ async function updateLog() { try { - let res = await fetch(url, { - headers: { - 'Range': `bytes=${bytesReceived}-`, - } - }); - if (res.ok) { - let text = await res.text(); - if (text.length > 0) { - awaitingCommandResponse = false; - if (bytesReceived === 0 || res.status !== 206) { - logger.innerHTML = ''; + if (!document.hidden) { + updateLogImmediately = false; + let res = await fetch(url, { + headers: { + 'Range': `bytes=${bytesReceived}-`, } - if (res.status !== 206) { - bytesReceived = 0; - } else { - bytesReceived += new Blob([text]).size; + }); + if (res.ok) { + let text = await res.text(); + if (text.length > 0) { + awaitingCommandResponse = false; + if (bytesReceived === 0 || res.status !== 206) { + logger.innerHTML = ''; + } + if (res.status !== 206) { + bytesReceived = 0; + } else { + bytesReceived += new Blob([text]).size; + } + if (logger.innerHTML.endsWith('…')) { + logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); + } + logger.appendChild(document.createTextNode(text)); + scrollToBottom(logger); + let loader = document.getElementById("command-form").getElementsByClassName("loader")[0]; + loader.classList.remove("loading"); } - if (logger.innerHTML.endsWith('…')) { - logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); - } - logger.appendChild(document.createTextNode(text)); - scrollToBottom(logger); } + } else { + updateLogImmediately = true; } } finally { @@ -125,20 +136,62 @@ }); ev.preventDefault(); // has to happen before first await form.reset(); - let res = await req; - if (res.ok || res.type === 'opaqueredirect') { - awaitingCommandResponse = true; - window.clearTimeout(updateLogTimeout); - updateLogTimeout = window.setTimeout(updateLog, 100); - } else { - window.alert(res.statusText); + let loader = form.getElementsByClassName("loader")[0]; + loader.classList.add("loading"); + try { + let res = await req; + if (res.ok || res.type === 'opaqueredirect') { + awaitingCommandResponse = true; + window.clearTimeout(updateLogTimeout); + updateLogTimeout = window.setTimeout(updateLog, 100); + } else { + loader.classList.remove("loading"); + window.alert(res.statusText); + } + } catch (e) { + console.error(e); + loader.classList.remove("loading"); + window.alert(e.message); } } document.getElementById("command-form").addEventListener("submit", postForm); updateLogTimeout = window.setTimeout(updateLog, 1000); logger.scrollTop = logger.scrollHeight; + document.addEventListener("visibilitychange", () => { + if (!document.hidden && updateLogImmediately) { + updateLog(); + } + }) {% endif %} +
{% endblock %} diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py index e9dae41dd06f..4aa83e3b1c6c 100644 --- a/test/webhost/test_host_room.py +++ b/test/webhost/test_host_room.py @@ -131,7 +131,8 @@ def test_host_room_own(self) -> None: 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 = self.client.get(url_for("host_room", room=self.room_id), + headers={"User-Agent": "Mozilla/5.0"}) response_text = response.get_data(True) self.assertEqual(response.status_code, 200) self.assertIn("href=\"/seed/", response_text) From 69487661ddfcf3500a7ebac270ac1b843b116625 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 17 Sep 2024 18:33:03 -0500 Subject: [PATCH 279/393] Core: change yaml_output to output a full csv (#3653) * make yaml_output arg a bool instead of number * make yaml_output dump all player options as csv * it sorts by game so swap those columns * capitalize game and name headers * use a list and just add an if before adding instead of sorting * skip options that the world doesn't want displayed * check if the class is a subclass of Removed specifically instead of the none flag * don't create empty rows * add a header for every game option that isn't from the common ones even if they have the same name * add to webhost gen args so it can still gen --- Generate.py | 31 +++++------------------------ Main.py | 3 +++ Options.py | 44 ++++++++++++++++++++++++++++++++++++++++-- WebHostLib/generate.py | 1 + 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/Generate.py b/Generate.py index 4eba05cc52fe..2488504f3049 100644 --- a/Generate.py +++ b/Generate.py @@ -43,10 +43,10 @@ def mystery_argparse(): parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--log_level', default='info', help='Sets log level') - parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0), - help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') - parser.add_argument('--plando', default=defaults.plando_options, - help='List of options that can be set manually. Can be combined, for example "bosses, items"') + parser.add_argument("--yaml_output", action="store_true", + help="Output rolled player options to csv (made for async multiworld).") + parser.add_argument("--plando", default=defaults.plando_options, + help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") parser.add_argument("--skip_output", action="store_true", @@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output erargs.name = {} + erargs.yaml_output = args.yaml_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}") - if args.yaml_output: - import yaml - important = {} - for option, player_settings in vars(erargs).items(): - if type(player_settings) == dict: - if all(type(value) != list for value in player_settings.values()): - if len(player_settings.values()) > 1: - important[option] = {player: value for player, value in player_settings.items() if - player <= args.yaml_output} - else: - logging.debug(f"No player settings defined for option '{option}'") - - else: - if player_settings != "": # is not empty name - important[option] = player_settings - else: - logging.debug(f"No player settings defined for option '{option}'") - if args.outputpath: - os.makedirs(args.outputpath, exist_ok=True) - with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: - yaml.dump(important, f) - return erargs, seed diff --git a/Main.py b/Main.py index c931e22145a5..c09a537b60bd 100644 --- a/Main.py +++ b/Main.py @@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) + if args.yaml_output: + from Options import dump_player_options + dump_player_options(multiworld) multiworld.set_item_links() multiworld.state = CollectionState(multiworld) logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) diff --git a/Options.py b/Options.py index ac4b2b8cd8bb..aa6f175fa58d 100644 --- a/Options.py +++ b/Options.py @@ -8,16 +8,17 @@ import random import typing import enum +from collections import defaultdict from copy import deepcopy from dataclasses import dataclass from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str +from Utils import get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: - from BaseClasses import PlandoOptions + from BaseClasses import MultiWorld, PlandoOptions from worlds.AutoWorld import World import pathlib @@ -1532,3 +1533,42 @@ def yaml_dump_scalar(scalar) -> str: with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) + + +def dump_player_options(multiworld: MultiWorld) -> None: + from csv import DictWriter + + game_players = defaultdict(list) + for player, game in multiworld.game.items(): + game_players[game].append(player) + game_players = dict(sorted(game_players.items())) + + output = [] + per_game_option_names = [ + getattr(option, "display_name", option_key) + for option_key, option in PerGameCommonOptions.type_hints.items() + ] + all_option_names = per_game_option_names.copy() + for game, players in game_players.items(): + game_option_names = per_game_option_names.copy() + for player in players: + world = multiworld.worlds[player] + player_output = { + "Game": multiworld.game[player], + "Name": multiworld.get_player_name(player), + } + output.append(player_output) + for option_key, option in world.options_dataclass.type_hints.items(): + if issubclass(Removed, option): + continue + display_name = getattr(option, "display_name", option_key) + player_output[display_name] = getattr(world.options, option_key).current_option_name + if display_name not in game_option_names: + all_option_names.append(display_name) + game_option_names.append(display_name) + + with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file: + fields = ["Game", "Name", *all_option_names] + writer = DictWriter(file, fields) + writer.writeheader() + writer.writerows(output) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a12dc0f4ae14..2daf212efc29 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -134,6 +134,7 @@ def task(): {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False + erargs.yaml_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From da781bb4ac29fd45f800bf4af605a2f6fa93afa5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 18 Sep 2024 04:37:10 +0200 Subject: [PATCH 280/393] Core: rename yaml_output to csv_output (#3955) --- Generate.py | 4 ++-- Main.py | 2 +- WebHostLib/generate.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Generate.py b/Generate.py index 2488504f3049..52babdf18839 100644 --- a/Generate.py +++ b/Generate.py @@ -43,7 +43,7 @@ def mystery_argparse(): parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--log_level', default='info', help='Sets log level') - parser.add_argument("--yaml_output", action="store_true", + parser.add_argument("--csv_output", action="store_true", help="Output rolled player options to csv (made for async multiworld).") parser.add_argument("--plando", default=defaults.plando_options, help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") @@ -156,7 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output erargs.name = {} - erargs.yaml_output = args.yaml_output + erargs.csv_output = args.csv_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) diff --git a/Main.py b/Main.py index c09a537b60bd..5a0f5c98bcc4 100644 --- a/Main.py +++ b/Main.py @@ -46,7 +46,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) - if args.yaml_output: + if args.csv_output: from Options import dump_player_options dump_player_options(multiworld) multiworld.set_item_links() diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 2daf212efc29..dbe7dd958910 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -134,7 +134,7 @@ def task(): {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False - erargs.yaml_output = False + erargs.csv_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From 710609fa602d3fb3cfa002caa2b1d31ce6fa6dbb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:27:53 +0200 Subject: [PATCH 281/393] WebHost: move api/room_status out of __init__.py (#3958) * WebHost: move room_status out of __init__.py The old location is unexpected and easy to miss. * WebHost: fix typing in api/room_status --- WebHostLib/api/__init__.py | 42 +++----------------------------------- WebHostLib/api/room.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 WebHostLib/api/room.py diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 4003243a281d..cf05e87374ab 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,51 +1,15 @@ """API endpoints package.""" from typing import List, Tuple -from uuid import UUID -from flask import Blueprint, abort, url_for +from flask import Blueprint -import worlds.Files -from ..models import Room, Seed +from ..models import Seed api_endpoints = Blueprint('api', __name__, url_prefix="/api") -# unsorted/misc endpoints - def get_players(seed: Seed) -> List[Tuple[str, str]]: return [(slot.player_name, slot.game) for slot in seed.slots] -@api_endpoints.route('/room_status/') -def room_info(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - - def supports_apdeltapatch(game: str): - return game in worlds.Files.AutoPatchRegister.patch_types - downloads = [] - for slot in sorted(room.seed.slots): - if slot.data and not supports_apdeltapatch(slot.game): - slot_download = { - "slot": slot.player_id, - "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) - } - downloads.append(slot_download) - elif slot.data: - slot_download = { - "slot": slot.player_id, - "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) - } - downloads.append(slot_download) - return { - "tracker": room.tracker, - "players": get_players(room.seed), - "last_port": room.last_port, - "last_activity": room.last_activity, - "timeout": room.timeout, - "downloads": downloads, - } - - -from . import generate, user, datapackage # trigger registration +from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/room.py b/WebHostLib/api/room.py new file mode 100644 index 000000000000..9337975695b2 --- /dev/null +++ b/WebHostLib/api/room.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +from uuid import UUID + +from flask import abort, url_for + +import worlds.Files +from . import api_endpoints, get_players +from ..models import Room + + +@api_endpoints.route('/room_status/') +def room_info(room_id: UUID) -> Dict[str, Any]: + room = Room.get(id=room_id) + if room is None: + return abort(404) + + def supports_apdeltapatch(game: str) -> bool: + return game in worlds.Files.AutoPatchRegister.patch_types + + downloads = [] + for slot in sorted(room.seed.slots): + if slot.data and not supports_apdeltapatch(slot.game): + slot_download = { + "slot": slot.player_id, + "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) + } + downloads.append(slot_download) + elif slot.data: + slot_download = { + "slot": slot.player_id, + "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) + } + downloads.append(slot_download) + + return { + "tracker": room.tracker, + "players": get_players(room.seed), + "last_port": room.last_port, + "last_activity": room.last_activity, + "timeout": room.timeout, + "downloads": downloads, + } From 51a6dc150c6d74fa80ce0cc4382b88dcadda58d5 Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Wed, 18 Sep 2024 13:33:02 -0400 Subject: [PATCH 282/393] MLSS: Various bugfixes and QoL updates (#3744) * Small fixes * Update Location names + Remove redundant rule * Fix for str not being returned in get_filler_item_name() * ASM changes + various name/logic updates * Remove extra unintended change + Make beanstone/beanlets useful * Add missing timer logic to client * Update Rules.py * Fix bad capitalization * Small formatting and ASM changes * Update basepatch.bsdiff * Update seed verification to be more likely to make a correct comparison * Add Pipe 10 * Final batch of small fixes * FINAL CHANGE I SWEAR * Added victory Item for spoilers * Update worlds/mlss/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix jokes end logic * Update worlds/mlss/Regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/mlss/Rules.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fix jokes end logic * Item Location mismatch + Check options against rules * Change List to Set + Check options against rules * Moved Victory item to event * Update worlds/mlss/__init__.py Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> * Update worlds/mlss/__init__.py Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/mlss/Client.py | 25 ++++- worlds/mlss/Data.py | 6 +- worlds/mlss/Items.py | 30 ++--- worlds/mlss/Locations.py | 117 +++++++++---------- worlds/mlss/Names/LocationName.py | 88 +++++++-------- worlds/mlss/Options.py | 5 +- worlds/mlss/Regions.py | 71 ++++++------ worlds/mlss/Rom.py | 33 +++--- worlds/mlss/Rules.py | 181 ++++++++++++++++++++++++++---- worlds/mlss/__init__.py | 65 ++++------- worlds/mlss/data/basepatch.bsdiff | Bin 17596 -> 18482 bytes 11 files changed, 377 insertions(+), 244 deletions(-) diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index 1f08b85610d6..75f6ac653003 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -85,7 +85,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: if not self.seed_verify: seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")]) seed = seed[0].decode("UTF-8") - if seed != ctx.seed_name: + if seed not in ctx.seed_name: logger.info( "ERROR: The ROM you loaded is for a different game of AP. " "Please make sure the host has sent you the correct patch file," @@ -143,17 +143,30 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # If RAM address isn't 0x0 yet break out and try again later to give the rest of the items for i in range(len(ctx.items_received) - received_index): item_data = items_by_id[ctx.items_received[received_index + i].item] - b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")]) - if b is None: + result = False + total = 0 + while not result: + await asyncio.sleep(0.05) + total += 0.05 + result = await bizhawk.guarded_write( + ctx.bizhawk_ctx, + [ + (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM") + ], + [(0x3057, [0x0], "EWRAM")] + ) + if result: + total = 0 + if total >= 1: + break + if not result: break await bizhawk.write( ctx.bizhawk_ctx, [ - (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"), (0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"), - ], + ] ) - await asyncio.sleep(0.1) # Early return and location send if you are currently in a shop, # since other flags aren't going to change diff --git a/worlds/mlss/Data.py b/worlds/mlss/Data.py index 749e63bcf24d..add14aa008f1 100644 --- a/worlds/mlss/Data.py +++ b/worlds/mlss/Data.py @@ -1,6 +1,9 @@ flying = [ 0x14, 0x1D, + 0x32, + 0x33, + 0x40, 0x4C ] @@ -23,7 +26,6 @@ 0x5032AC, 0x5032CC, 0x5032EC, - 0x50330C, 0x50332C, 0x50334C, 0x50336C, @@ -151,7 +153,7 @@ 0x50458C, 0x5045AC, 0x50468C, - 0x5046CC, + # 0x5046CC, 6 enemy formation 0x5046EC, 0x50470C ] diff --git a/worlds/mlss/Items.py b/worlds/mlss/Items.py index b95f1a0bc0a8..717443ddfc06 100644 --- a/worlds/mlss/Items.py +++ b/worlds/mlss/Items.py @@ -78,21 +78,21 @@ class MLSSItem(Item): ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67), ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70), ItemData(77771062, "Spangle", ItemClassification.progression, 0x72), - ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73), - ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74), - ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75), - ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76), - ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77), - ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80), - ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81), - ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82), - ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83), - ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84), - ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85), - ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86), - ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87), - ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90), - ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91), + ItemData(77771063, "Beanlet 1", ItemClassification.useful, 0x73), + ItemData(77771064, "Beanlet 2", ItemClassification.useful, 0x74), + ItemData(77771065, "Beanlet 3", ItemClassification.useful, 0x75), + ItemData(77771066, "Beanlet 4", ItemClassification.useful, 0x76), + ItemData(77771067, "Beanlet 5", ItemClassification.useful, 0x77), + ItemData(77771068, "Beanstone 1", ItemClassification.useful, 0x80), + ItemData(77771069, "Beanstone 2", ItemClassification.useful, 0x81), + ItemData(77771070, "Beanstone 3", ItemClassification.useful, 0x82), + ItemData(77771071, "Beanstone 4", ItemClassification.useful, 0x83), + ItemData(77771072, "Beanstone 5", ItemClassification.useful, 0x84), + ItemData(77771073, "Beanstone 6", ItemClassification.useful, 0x85), + ItemData(77771074, "Beanstone 7", ItemClassification.useful, 0x86), + ItemData(77771075, "Beanstone 8", ItemClassification.useful, 0x87), + ItemData(77771076, "Beanstone 9", ItemClassification.useful, 0x90), + ItemData(77771077, "Beanstone 10", ItemClassification.useful, 0x91), ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92), ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93), ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F), diff --git a/worlds/mlss/Locations.py b/worlds/mlss/Locations.py index 8c00432a8f06..a2787ef9b1b1 100644 --- a/worlds/mlss/Locations.py +++ b/worlds/mlss/Locations.py @@ -4,9 +4,6 @@ class LocationData: - name: str = "" - id: int = 0x00 - def __init__(self, name, id_, itemType): self.name = name self.itemType = itemType @@ -93,8 +90,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0), LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0), LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 1", 0x39D890, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 2", 0x39D8A0, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0), LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0), @@ -104,7 +101,7 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0), LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0), LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0), - LocationData("Hoohoo Village Right Side Block", 0x39D957, 0), + LocationData("Hoohoo Village Eastside Block", 0x39D957, 0), LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0), LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0), LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0), @@ -119,8 +116,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 1", 0x39DA35, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 2", 0x39DA2D, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0), LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0), @@ -143,14 +140,14 @@ class MLSSLocation(Location): LocationData("Shop Starting Flag 3", 0x3C05F4, 3), LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0), LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0), - LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0), + LocationData("Hoohoo Mountain Past Hoohooros Digspot", 0x39D898, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0), LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0), LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0), LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0), LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0), - LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0), + LocationData("Hoohoo Village Eastside Digspot", 0x39D95F, 0), LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0), LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0), LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0), @@ -267,7 +264,7 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0), LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0), LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0), - LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0), LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0), LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0), LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0), @@ -286,11 +283,12 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0), - LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0), + LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Right Side)", 0x39D9E9, 0), LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1), LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0), + LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0), LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0), @@ -345,12 +343,12 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 1", 0x39DEE4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 2", 0x39DEEC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 3", 0x39DEF4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 4", 0x39DEFC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 5", 0x39DF04, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 6", 0x39DF0C, 0), LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0), LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0), LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0), @@ -367,14 +365,14 @@ class MLSSLocation(Location): ] castleTown: typing.List[LocationData] = [ - LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0), - LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0), - LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0), - LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0), - LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0), - LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0), - LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0), - LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0), + LocationData("Beanbean Castle Town West Side House Block 1", 0x39D7A4, 0), + LocationData("Beanbean Castle Town West Side House Block 2", 0x39D7AC, 0), + LocationData("Beanbean Castle Town West Side House Block 3", 0x39D7B4, 0), + LocationData("Beanbean Castle Town West Side House Block 4", 0x39D7BC, 0), + LocationData("Beanbean Castle Town East Side House Block 1", 0x39D7D8, 0), + LocationData("Beanbean Castle Town East Side House Block 2", 0x39D7E0, 0), + LocationData("Beanbean Castle Town East Side House Block 3", 0x39D7E8, 0), + LocationData("Beanbean Castle Town East Side House Block 4", 0x39D7F0, 0), LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2), LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2), LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1), @@ -444,14 +442,14 @@ class MLSSLocation(Location): ] kidnappedFlag: typing.List[LocationData] = [ - LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2), - LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2), - LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2), - LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2), - LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2), - LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2), - LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3), - LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3), + LocationData("Badge Shop Trunkle Flag 1", 0x3C0640, 2), + LocationData("Badge Shop Trunkle Flag 2", 0x3C0642, 2), + LocationData("Badge Shop Trunkle Flag 3", 0x3C0644, 2), + LocationData("Pants Shop Trunkle Flag 1", 0x3C0646, 2), + LocationData("Pants Shop Trunkle Flag 2", 0x3C0648, 2), + LocationData("Pants Shop Trunkle Flag 3", 0x3C064A, 2), + LocationData("Shop Trunkle Flag 1", 0x3C0606, 3), + LocationData("Shop Trunkle Flag 2", 0x3C0608, 3), ] beanstarFlag: typing.List[LocationData] = [ @@ -553,21 +551,21 @@ class MLSSLocation(Location): airport: typing.List[LocationData] = [ LocationData("Airport Entrance Digspot", 0x39E2DC, 0), LocationData("Airport Lobby Digspot", 0x39E2E9, 0), - LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0), - LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0), - LocationData("Airport Leftside Digspot 3", 0x39E306, 0), - LocationData("Airport Leftside Digspot 4", 0x39E30E, 0), - LocationData("Airport Leftside Digspot 5", 0x39E316, 0), + LocationData("Airport Westside Digspot 1", 0x39E2F6, 0), + LocationData("Airport Westside Digspot 2", 0x39E2FE, 0), + LocationData("Airport Westside Digspot 3", 0x39E306, 0), + LocationData("Airport Westside Digspot 4", 0x39E30E, 0), + LocationData("Airport Westside Digspot 5", 0x39E316, 0), LocationData("Airport Center Digspot 1", 0x39E323, 0), LocationData("Airport Center Digspot 2", 0x39E32B, 0), LocationData("Airport Center Digspot 3", 0x39E333, 0), LocationData("Airport Center Digspot 4", 0x39E33B, 0), LocationData("Airport Center Digspot 5", 0x39E343, 0), - LocationData("Airport Rightside Digspot 1", 0x39E350, 0), - LocationData("Airport Rightside Digspot 2", 0x39E358, 0), - LocationData("Airport Rightside Digspot 3", 0x39E360, 0), - LocationData("Airport Rightside Digspot 4", 0x39E368, 0), - LocationData("Airport Rightside Digspot 5", 0x39E370, 0), + LocationData("Airport Eastside Digspot 1", 0x39E350, 0), + LocationData("Airport Eastside Digspot 2", 0x39E358, 0), + LocationData("Airport Eastside Digspot 3", 0x39E360, 0), + LocationData("Airport Eastside Digspot 4", 0x39E368, 0), + LocationData("Airport Eastside Digspot 5", 0x39E370, 0), ] gwarharEntrance: typing.List[LocationData] = [ @@ -617,7 +615,6 @@ class MLSSLocation(Location): LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0), LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0), LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0), LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0), @@ -667,7 +664,7 @@ class MLSSLocation(Location): LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0), - LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0), + LocationData("Bowser's Castle Past Morton Block", 0x39EA0C, 0), LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0), LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0), LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0), @@ -705,16 +702,16 @@ class MLSSLocation(Location): LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0), LocationData("Joke's End Mole Reward 1", 0x27788E, 1), LocationData("Joke's End Mole Reward 2", 0x2778D2, 1), -] - -jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0), LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0), LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0), LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0), - LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0), LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0), + LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), +] + +jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0), LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0), LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0), @@ -740,10 +737,10 @@ class MLSSLocation(Location): postJokes: typing.List[LocationData] = [ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0), - LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0), - LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0), - LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0), - LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0), + LocationData("Teehee Valley Before Birdo Digspot 1", 0x39E55B, 0), + LocationData("Teehee Valley Before Birdo Digspot 2", 0x39E563, 0), + LocationData("Teehee Valley Before Birdo Digspot 3", 0x39E56B, 0), + LocationData("Teehee Valley Before Birdo Digspot 4", 0x39E573, 0), ] theater: typing.List[LocationData] = [ @@ -766,6 +763,10 @@ class MLSSLocation(Location): LocationData("Oho Oasis Thunderhand", 0x1E9409, 2), ] +cacklettas_soul: typing.List[LocationData] = [ + LocationData("Cackletta's Soul", None, 0), +] + nonBlock = [ (0x434B, 0x1, 0x243844), # Farm Mole 1 (0x434B, 0x1, 0x24387D), # Farm Mole 2 @@ -1171,15 +1172,15 @@ class MLSSLocation(Location): + fungitownBeanstar + fungitownBirdo + bowsers + + bowsersMini + jokesEntrance + jokesMain + postJokes + theater + oasis + gwarharMain - + bowsersMini + baseUltraRocks + coins ) -location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations} +location_table: typing.Dict[str, int] = {location.name: location.id for location in all_locations} diff --git a/worlds/mlss/Names/LocationName.py b/worlds/mlss/Names/LocationName.py index 7cbc2e4f31f8..5b38b2a10f6e 100644 --- a/worlds/mlss/Names/LocationName.py +++ b/worlds/mlss/Names/LocationName.py @@ -8,14 +8,14 @@ class LocationName: StardustFields4Block3 = "Stardust Fields Room 4 Block 3" StardustFields5Block = "Stardust Fields Room 5 Block" HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block" - BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1" - BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2" - BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3" - BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4" - BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1" - BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2" - BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3" - BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4" + BeanbeanCastleTownWestsideHouseBlock1 = "Beanbean Castle Town Westside House Block 1" + BeanbeanCastleTownWestsideHouseBlock2 = "Beanbean Castle Town Westside House Block 2" + BeanbeanCastleTownWestsideHouseBlock3 = "Beanbean Castle Town Westside House Block 3" + BeanbeanCastleTownWestsideHouseBlock4 = "Beanbean Castle Town Westside House Block 4" + BeanbeanCastleTownEastsideHouseBlock1 = "Beanbean Castle Town Eastside House Block 1" + BeanbeanCastleTownEastsideHouseBlock2 = "Beanbean Castle Town Eastside House Block 2" + BeanbeanCastleTownEastsideHouseBlock3 = "Beanbean Castle Town Eastside House Block 3" + BeanbeanCastleTownEastsideHouseBlock4 = "Beanbean Castle Town Eastside House Block 4" BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1" BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2" BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3" @@ -26,9 +26,9 @@ class LocationName: HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1" HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2" HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3" - HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1" - HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot" - HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2" + HoohooMountainPastHoohoorosBlock1 = "Hoohoo Mountain Past Hoohooros Block 1" + HoohooMountainPastHoohoorosDigspot = "Hoohoo Mountain Past Hoohooros Digspot" + HoohooMountainPastHoohoorosBlock2 = "Hoohoo Mountain Past Hoohooros Block 2" HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1" HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2" HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1" @@ -44,8 +44,8 @@ class LocationName: HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3" HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block" HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot" - HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block" - HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot" + HoohooVillageEastsideBlock = "Hoohoo Village Eastside Block" + HoohooVillageEastsideDigspot = "Hoohoo Village Eastside Digspot" HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1" HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2" HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3" @@ -65,8 +65,8 @@ class LocationName: HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block" - HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1" - HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2" + HoohooMountainBasePastMinecartMinigameBlock1 = "Hoohoo Mountain Base Past Minecart Minigame Block 1" + HoohooMountainBasePastMinecartMinigameBlock2 = "Hoohoo Mountain Base Past Minecart Minigame Block 2" HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1" HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2" HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3" @@ -148,12 +148,12 @@ class LocationName: ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block" ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1" ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2" - ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1" - ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2" - ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3" - ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4" - ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5" - ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6" + ChucklehuckWoodsPastChucklerootBlock1 = "Chucklehuck Woods Past Chuckleroot Block 1" + ChucklehuckWoodsPastChucklerootBlock2 = "Chucklehuck Woods Past Chuckleroot Block 2" + ChucklehuckWoodsPastChucklerootBlock3 = "Chucklehuck Woods Past Chuckleroot Block 3" + ChucklehuckWoodsPastChucklerootBlock4 = "Chucklehuck Woods Past Chuckleroot Block 4" + ChucklehuckWoodsPastChucklerootBlock5 = "Chucklehuck Woods Past Chuckleroot Block 5" + ChucklehuckWoodsPastChucklerootBlock6 = "Chucklehuck Woods Past Chuckleroot Block 6" WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block" WinkleAreaDigspot = "Winkle Area Digspot" WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block" @@ -232,21 +232,21 @@ class LocationName: WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot" AirportEntranceDigspot = "Airport Entrance Digspot" AirportLobbyDigspot = "Airport Lobby Digspot" - AirportLeftsideDigspot1 = "Airport Leftside Digspot 1" - AirportLeftsideDigspot2 = "Airport Leftside Digspot 2" - AirportLeftsideDigspot3 = "Airport Leftside Digspot 3" - AirportLeftsideDigspot4 = "Airport Leftside Digspot 4" - AirportLeftsideDigspot5 = "Airport Leftside Digspot 5" + AirportWestsideDigspot1 = "Airport Westside Digspot 1" + AirportWestsideDigspot2 = "Airport Westside Digspot 2" + AirportWestsideDigspot3 = "Airport Westside Digspot 3" + AirportWestsideDigspot4 = "Airport Westside Digspot 4" + AirportWestsideDigspot5 = "Airport Westside Digspot 5" AirportCenterDigspot1 = "Airport Center Digspot 1" AirportCenterDigspot2 = "Airport Center Digspot 2" AirportCenterDigspot3 = "Airport Center Digspot 3" AirportCenterDigspot4 = "Airport Center Digspot 4" AirportCenterDigspot5 = "Airport Center Digspot 5" - AirportRightsideDigspot1 = "Airport Rightside Digspot 1" - AirportRightsideDigspot2 = "Airport Rightside Digspot 2" - AirportRightsideDigspot3 = "Airport Rightside Digspot 3" - AirportRightsideDigspot4 = "Airport Rightside Digspot 4" - AirportRightsideDigspot5 = "Airport Rightside Digspot 5" + AirportEastsideDigspot1 = "Airport Eastside Digspot 1" + AirportEastsideDigspot2 = "Airport Eastside Digspot 2" + AirportEastsideDigspot3 = "Airport Eastside Digspot 3" + AirportEastsideDigspot4 = "Airport Eastside Digspot 4" + AirportEastsideDigspot5 = "Airport Eastside Digspot 5" GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot" GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot" GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot" @@ -276,10 +276,10 @@ class LocationName: WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block" WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1" WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2" - TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1" - TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2" - TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3" - TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4" + TeeheeValleyBeforeBirdoDigspot1 = "Teehee Valley Before Birdo Digspot 1" + TeeheeValleyBeforeBirdoDigspot2 = "Teehee Valley Before Birdo Digspot 2" + TeeheeValleyBeforeBirdoDigspot3 = "Teehee Valley Before Birdo Digspot 3" + TeeheeValleyBeforeBirdoDigspot4 = "Teehee Valley Before Birdo Digspot 4" TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1" TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2" TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3" @@ -296,9 +296,9 @@ class LocationName: TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)" TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3" TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot" - TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1" - TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2" - TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block" + TeeheeValleyUpperMazeRoom2Digspot1 = "Teehee Valley Upper Maze Room 2 Digspot 1" + TeeheeValleyUpperMazeRoom2Digspot2 = "Teehee Valley Upper Maze Room 2 Digspot 2" + TeeheeValleyUpperMazeRoom1Block = "Teehee Valley Upper Maze Room 1 Block" TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot" TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot" SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1" @@ -314,10 +314,10 @@ class LocationName: JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1" JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2" JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3" - JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block" - JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot" - JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block" - JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot" + JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast of Boiler Room 1 Block" + JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast of Boiler Room 3 Digspot" + JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast of Boiler Room 2 Block" + JokesEndNortheastOfBoilerRoom2Digspot = "Joke's End Northeast of Boiler Room 2 Digspot" JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1" JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2" JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3" @@ -505,7 +505,7 @@ class LocationName: BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1" BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2" BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot" - BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block" + BowsersCastlePastMortonBlock = "Bowser's Castle Past Morton Block" BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1" BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2" BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1" @@ -546,7 +546,7 @@ class LocationName: ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block" ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block" ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block" - ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block" + ChucklehuckWoodsPastChucklerootCoinBlock = "Chucklehuck Woods Past Chuckleroot Coin Block" ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block" ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block" SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block" diff --git a/worlds/mlss/Options.py b/worlds/mlss/Options.py index 14c1ef3a7d5a..73e8ebd4015f 100644 --- a/worlds/mlss/Options.py +++ b/worlds/mlss/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range +from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range, Removed from dataclasses import dataclass @@ -282,7 +282,8 @@ class MLSSOptions(PerGameCommonOptions): extra_pipes: ExtraPipes skip_minecart: SkipMinecart disable_surf: DisableSurf - harhalls_pants: HarhallsPants + disable_harhalls_pants: HarhallsPants + harhalls_pants: Removed block_visibility: HiddenVisible chuckle_beans: ChuckleBeans music_options: MusicOptions diff --git a/worlds/mlss/Regions.py b/worlds/mlss/Regions.py index 992e99e2c7f7..7dd5e9451141 100644 --- a/worlds/mlss/Regions.py +++ b/worlds/mlss/Regions.py @@ -33,6 +33,7 @@ postJokes, baseUltraRocks, coins, + cacklettas_soul, ) from . import StateLogic @@ -40,44 +41,45 @@ from . import MLSSWorld -def create_regions(world: "MLSSWorld", excluded: typing.List[str]): +def create_regions(world: "MLSSWorld"): menu_region = Region("Menu", world.player, world.multiworld) world.multiworld.regions.append(menu_region) - create_region(world, "Main Area", mainArea, excluded) - create_region(world, "Chucklehuck Woods", chucklehuck, excluded) - create_region(world, "Beanbean Castle Town", castleTown, excluded) - create_region(world, "Shop Starting Flag", startingFlag, excluded) - create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded) - create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded) - create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded) - create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded) - create_region(world, "Shop Birdo Flag", birdoFlag, excluded) - create_region(world, "Surfable", surfable, excluded) - create_region(world, "Hooniversity", hooniversity, excluded) - create_region(world, "GwarharEntrance", gwarharEntrance, excluded) - create_region(world, "GwarharMain", gwarharMain, excluded) - create_region(world, "TeeheeValley", teeheeValley, excluded) - create_region(world, "Winkle", winkle, excluded) - create_region(world, "Sewers", sewers, excluded) - create_region(world, "Airport", airport, excluded) - create_region(world, "JokesEntrance", jokesEntrance, excluded) - create_region(world, "JokesMain", jokesMain, excluded) - create_region(world, "PostJokes", postJokes, excluded) - create_region(world, "Theater", theater, excluded) - create_region(world, "Fungitown", fungitown, excluded) - create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded) - create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded) - create_region(world, "BooStatue", booStatue, excluded) - create_region(world, "Oasis", oasis, excluded) - create_region(world, "BaseUltraRocks", baseUltraRocks, excluded) + create_region(world, "Main Area", mainArea) + create_region(world, "Chucklehuck Woods", chucklehuck) + create_region(world, "Beanbean Castle Town", castleTown) + create_region(world, "Shop Starting Flag", startingFlag) + create_region(world, "Shop Chuckolator Flag", chuckolatorFlag) + create_region(world, "Shop Mom Piranha Flag", piranhaFlag) + create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag) + create_region(world, "Shop Beanstar Complete Flag", beanstarFlag) + create_region(world, "Shop Birdo Flag", birdoFlag) + create_region(world, "Surfable", surfable) + create_region(world, "Hooniversity", hooniversity) + create_region(world, "GwarharEntrance", gwarharEntrance) + create_region(world, "GwarharMain", gwarharMain) + create_region(world, "TeeheeValley", teeheeValley) + create_region(world, "Winkle", winkle) + create_region(world, "Sewers", sewers) + create_region(world, "Airport", airport) + create_region(world, "JokesEntrance", jokesEntrance) + create_region(world, "JokesMain", jokesMain) + create_region(world, "PostJokes", postJokes) + create_region(world, "Theater", theater) + create_region(world, "Fungitown", fungitown) + create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar) + create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo) + create_region(world, "BooStatue", booStatue) + create_region(world, "Oasis", oasis) + create_region(world, "BaseUltraRocks", baseUltraRocks) + create_region(world, "Cackletta's Soul", cacklettas_soul) if world.options.coins: - create_region(world, "Coins", coins, excluded) + create_region(world, "Coins", coins) if not world.options.castle_skip: - create_region(world, "Bowser's Castle", bowsers, excluded) - create_region(world, "Bowser's Castle Mini", bowsersMini, excluded) + create_region(world, "Bowser's Castle", bowsers) + create_region(world, "Bowser's Castle Mini", bowsersMini) def connect_regions(world: "MLSSWorld"): @@ -221,6 +223,9 @@ def connect_regions(world: "MLSSWorld"): "Bowser's Castle Mini", lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player), ) + connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul") + else: + connect(world, names, "PostJokes", "Cackletta's Soul") connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player)) connect( world, @@ -282,11 +287,11 @@ def connect_regions(world: "MLSSWorld"): ) -def create_region(world: "MLSSWorld", name, locations, excluded): +def create_region(world: "MLSSWorld", name, locations): ret = Region(name, world.player, world.multiworld) for location in locations: loc = MLSSLocation(world.player, location.name, location.id, ret) - if location.name in excluded: + if location.name in world.disabled_locations: continue ret.locations.append(loc) world.multiworld.regions.append(ret) diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py index 7cbbe8875195..03eac040efb2 100644 --- a/worlds/mlss/Rom.py +++ b/worlds/mlss/Rom.py @@ -8,7 +8,7 @@ from settings import get_settings from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension from .Items import item_table -from .Locations import shop, badge, pants, location_table, hidden, all_locations +from .Locations import shop, badge, pants, location_table, all_locations if TYPE_CHECKING: from . import MLSSWorld @@ -88,7 +88,7 @@ def hidden_visible(caller: APProcedurePatch, rom: bytes): return rom stream = io.BytesIO(rom) - for location in all_locations: + for location in [location for location in all_locations if location.itemType == 0]: stream.seek(location.id - 6) b = stream.read(1) if b[0] == 0x10 and options["block_visibility"] == 1: @@ -133,7 +133,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): stream = io.BytesIO(rom) random.seed(options["seed"] + options["player"]) - if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0: + if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0): raw = [] for pos in bosses: stream.seek(pos + 1) @@ -164,6 +164,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): enemies_raw = [] groups = [] + boss_groups = [] if options["randomize_enemies"] == 0: return stream.getvalue() @@ -171,7 +172,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): if options["randomize_bosses"] == 2: for pos in bosses: stream.seek(pos + 1) - groups += [stream.read(0x1F)] + boss_groups += [stream.read(0x1F)] for pos in enemies: stream.seek(pos + 8) @@ -221,12 +222,19 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): groups += [raw] chomp = False - random.shuffle(groups) arr = enemies if options["randomize_bosses"] == 2: arr += bosses + groups += boss_groups + + random.shuffle(groups) for pos in arr: + if arr[-1] in boss_groups: + stream.seek(pos) + temp = stream.read(1) + stream.seek(pos) + stream.write(bytes([temp[0] | 0x8])) stream.seek(pos + 1) stream.write(groups.pop()) @@ -320,20 +328,9 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None: patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)])) for location_name in location_table.keys(): - if ( - (world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name) - or (world.options.castle_skip and "Bowser" in location_name) - or (world.options.disable_surf and "Surf Minigame" in location_name) - or (world.options.harhalls_pants and "Harhall's" in location_name) - ): - continue - if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or ( - world.options.chuckle_beans == 1 and location_table[location_name] in hidden - ): - continue - if not world.options.coins and "Coin" in location_name: + if location_name in world.disabled_locations: continue - location = world.multiworld.get_location(location_name, world.player) + location = world.get_location(location_name) item = location.item address = [address for address in all_locations if address.name == location.name] item_inject(world, patch, location.address, address[0].itemType, item) diff --git a/worlds/mlss/Rules.py b/worlds/mlss/Rules.py index 13627eafc479..b0b5a36465e2 100644 --- a/worlds/mlss/Rules.py +++ b/worlds/mlss/Rules.py @@ -13,7 +13,7 @@ def set_rules(world: "MLSSWorld", excluded): for location in all_locations: if "Digspot" in location.name: if (world.options.skip_minecart and "Minecart" in location.name) or ( - world.options.castle_skip and "Bowser" in location.name + world.options.castle_skip and "Bowser" in location.name ): continue if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden: @@ -218,9 +218,9 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade), lambda state: StateLogic.thunder(state, world.player) - and StateLogic.pieces(state, world.player) - and StateLogic.castleTown(state, world.player) - and StateLogic.rose(state, world.player), + and StateLogic.pieces(state, world.player) + and StateLogic.castleTown(state, world.player) + and StateLogic.rose(state, world.player), ) add_rule( world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole), @@ -235,27 +235,27 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock1), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock2), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock3), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock4), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock5), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock6), lambda state: StateLogic.fruits(state, world.player), ) add_rule( @@ -350,10 +350,6 @@ def set_rules(world: "MLSSWorld", excluded): world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2), lambda state: StateLogic.ultra(state, world.player), ) - add_rule( - world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block), - lambda state: StateLogic.ultra(state, world.player), - ) add_rule( world.get_location(LocationName.OhoOasisFirebrand), lambda state: StateLogic.canMini(state, world.player), @@ -462,6 +458,143 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canCrash(state, world.player), ) + if world.options.randomize_bosses.value != 0: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + if world.options.chuckle_beans == 2: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooVillageHammers), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPeasleysRose), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock3), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomBlock), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + + if not world.options.difficult_logic: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom3Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom1Block), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block2), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block3), + lambda state: StateLogic.canCrash(state, world.player), + ) + if world.options.coins: add_rule( world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1), @@ -516,7 +649,7 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootCoinBlock), lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player), ) add_rule( @@ -546,23 +679,23 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock), lambda state: StateLogic.canDash(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), + and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), ) add_rule( world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and ( - StateLogic.membership(state, world.player) - or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player)) - ), + and StateLogic.fire(state, world.player) + and (StateLogic.membership(state, world.player) + or (StateLogic.canDig(state, world.player) + and StateLogic.canMini(state, world.player))), ) add_rule( world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and StateLogic.canDig(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)), + and StateLogic.fire(state, world.player) + and StateLogic.canDig(state, world.player) + and (StateLogic.membership(state, world.player) + or StateLogic.canMini(state, world.player)), ) if not world.options.difficult_logic: add_rule( diff --git a/worlds/mlss/__init__.py b/worlds/mlss/__init__.py index f44343c230d0..bb7ed0515419 100644 --- a/worlds/mlss/__init__.py +++ b/worlds/mlss/__init__.py @@ -4,7 +4,7 @@ import settings from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World -from typing import List, Dict, Any +from typing import Set, Dict, Any from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins from .Options import MLSSOptions from .Items import MLSSItem, itemList, item_frequencies, item_table @@ -55,29 +55,29 @@ class MLSSWorld(World): settings: typing.ClassVar[MLSSSettings] item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 0) - disabled_locations: List[str] + disabled_locations: Set[str] def generate_early(self) -> None: - self.disabled_locations = [] - if self.options.chuckle_beans == 0: - self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name] - if self.options.castle_skip: - self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name] - if self.options.chuckle_beans == 1: - self.disabled_locations = [location.name for location in all_locations if location.id in hidden] + self.disabled_locations = set() if self.options.skip_minecart: - self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot] + self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot]) if self.options.disable_surf: - self.disabled_locations += [LocationName.SurfMinigame] - if self.options.harhalls_pants: - self.disabled_locations += [LocationName.HarhallsPants] + self.disabled_locations.update([LocationName.SurfMinigame]) + if self.options.disable_harhalls_pants: + self.disabled_locations.update([LocationName.HarhallsPants]) + if self.options.chuckle_beans == 0: + self.disabled_locations.update([location.name for location in all_locations if "Digspot" in location.name]) + if self.options.chuckle_beans == 1: + self.disabled_locations.update([location.name for location in all_locations if location.id in hidden]) + if self.options.castle_skip: + self.disabled_locations.update([location.name for location in bowsers + bowsersMini]) if not self.options.coins: - self.disabled_locations += [location.name for location in all_locations if location in coins] + self.disabled_locations.update([location.name for location in coins]) def create_regions(self) -> None: - create_regions(self, self.disabled_locations) + create_regions(self) connect_regions(self) item = self.create_item("Mushroom") @@ -90,13 +90,15 @@ def create_regions(self) -> None: self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item) item = self.create_item("Chuckle Bean") self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item) + item = MLSSItem("Victory", ItemClassification.progression, None, self.player) + self.get_location("Cackletta's Soul").place_locked_item(item) def fill_slot_data(self) -> Dict[str, Any]: return { "CastleSkip": self.options.castle_skip.value, "SkipMinecart": self.options.skip_minecart.value, "DisableSurf": self.options.disable_surf.value, - "HarhallsPants": self.options.harhalls_pants.value, + "HarhallsPants": self.options.disable_harhalls_pants.value, "ChuckleBeans": self.options.chuckle_beans.value, "DifficultLogic": self.options.difficult_logic.value, "Coins": self.options.coins.value, @@ -111,7 +113,7 @@ def create_items(self) -> None: freq = item_frequencies.get(item.itemName, 1) if item in precollected: freq = max(freq - precollected.count(item), 0) - if self.options.harhalls_pants and "Harhall's" in item.itemName: + if self.options.disable_harhalls_pants and "Harhall's" in item.itemName: continue required_items += [item.itemName for _ in range(freq)] @@ -135,21 +137,7 @@ def create_items(self) -> None: filler_items += [item.itemName for _ in range(freq)] # And finally take as many fillers as we need to have the same amount of items and locations. - remaining = len(all_locations) - len(required_items) - 5 - if self.options.castle_skip: - remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0) - if self.options.skip_minecart and self.options.chuckle_beans == 2: - remaining -= 1 - if self.options.disable_surf: - remaining -= 1 - if self.options.harhalls_pants: - remaining -= 1 - if self.options.chuckle_beans == 0: - remaining -= 192 - if self.options.chuckle_beans == 1: - remaining -= 59 - if not self.options.coins: - remaining -= len(coins) + remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5 self.multiworld.itempool += [ self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining) @@ -157,21 +145,14 @@ def create_items(self) -> None: def set_rules(self) -> None: set_rules(self, self.disabled_locations) - if self.options.castle_skip: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "PostJokes", "Region", self.player - ) - else: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "Bowser's Castle Mini", "Region", self.player - ) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) def create_item(self, name: str) -> MLSSItem: item = item_table[name] return MLSSItem(item.itemName, item.classification, item.code, self.player) def get_filler_item_name(self) -> str: - return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))) + return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))).itemName def generate_output(self, output_directory: str) -> None: patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 8f9324995ec4c9ef9992397d64b33847c207600d..7ed6c38ea9f432dfcf506156c77e4f56bdf3026a 100644 GIT binary patch literal 18482 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 literal 17596 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 From db5d9fbf70d89643bf766566c6d944e064369f5a Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:37:17 -0400 Subject: [PATCH 283/393] Pokemon R/B: Version 5 Update (#3566) * Quiz updates * Enable Partial Trainersanity * Losable Key Items Still Count * New options api * Type Chart Seed * Continue switching to new options API * Level Scaling and Quiz fixes * Level Scaling and Quiz fixes * Clarify that palettes are only for Super Gameboy * Type chart seed groups use one random players' options * remove goal option again * Text updates * Trainersanity Trainers ignore Blind Trainers setting * Re-order simple connecting interiors so that directions are preserved when possible * Dexsanity exact number * Year update * Dexsanity Doc update * revert accidental file deletion * Fixes * Add world parameter to logic calls * restore correct seeded random object * missing world.options changes * Trainersanity table bug fix * delete entrances as well as exits when restarting door shuffle * Do not collect route 25 item for level scaling if trainer is trainersanity * world.options in level_scaling.py * Update worlds/pokemon_rb/level_scaling.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/pokemon_rb/encounters.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update worlds/pokemon_rb/encounters.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * world -> multiworld * Fix Cerulean Cave Hidden Item Center Rocks region * Fix Cerulean Cave Hidden Item Center Rocks region for real * Remove "self-locking" rules * Update worlds/pokemon_rb/regions.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Fossil events * Update worlds/pokemon_rb/level_scaling.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: alchav Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/pokemon_rb/__init__.py | 416 ++++++------ worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 46356 -> 47245 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 46344 -> 47212 bytes .../docs/en_Pokemon Red and Blue.md | 9 +- worlds/pokemon_rb/encounters.py | 100 +-- worlds/pokemon_rb/items.py | 2 + worlds/pokemon_rb/level_scaling.py | 19 +- worlds/pokemon_rb/locations.py | 46 +- worlds/pokemon_rb/logic.py | 79 ++- worlds/pokemon_rb/options.py | 253 ++++---- worlds/pokemon_rb/pokemon.py | 217 ++++--- worlds/pokemon_rb/regions.py | 594 +++++++++--------- worlds/pokemon_rb/rom.py | 378 ++++++----- worlds/pokemon_rb/rom_addresses.py | 450 +++++++------ worlds/pokemon_rb/rules.py | 207 +++--- 15 files changed, 1435 insertions(+), 1335 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index c1d843189820..2065507e0d59 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -3,6 +3,7 @@ import typing import threading import base64 +import random from copy import deepcopy from typing import TextIO @@ -14,7 +15,7 @@ from .items import item_table, item_groups from .locations import location_data, PokemonRBLocation from .regions import create_regions -from .options import pokemon_rb_options +from .options import PokemonRBOptions from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch @@ -71,7 +72,10 @@ class PokemonRedBlueWorld(World): Elite Four to become the champion!""" # -MuffinJets#4559 game = "Pokemon Red and Blue" - option_definitions = pokemon_rb_options + + options_dataclass = PokemonRBOptions + options: PokemonRBOptions + settings: typing.ClassVar[PokemonSettings] required_client_version = (0, 4, 2) @@ -85,8 +89,8 @@ class PokemonRedBlueWorld(World): web = PokemonWebWorld() - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.item_pool = [] self.total_key_items = None self.fly_map = None @@ -101,11 +105,11 @@ def __init__(self, world: MultiWorld, player: int): self.learnsets = None self.trainer_name = None self.rival_name = None - self.type_chart = None self.traps = None self.trade_mons = {} self.finished_level_scaling = threading.Event() self.dexsanity_table = [] + self.trainersanity_table = [] self.local_locs = [] @classmethod @@ -113,11 +117,109 @@ def stage_assert_generate(cls, multiworld: MultiWorld): versions = set() for player in multiworld.player_ids: if multiworld.worlds[player].game == "Pokemon Red and Blue": - versions.add(multiworld.game_version[player].current_key) + versions.add(multiworld.worlds[player].options.game_version.current_key) for version in versions: if not os.path.exists(get_base_rom_path(version)): raise FileNotFoundError(get_base_rom_path(version)) + @classmethod + def stage_generate_early(cls, multiworld: MultiWorld): + + seed_groups = {} + pokemon_rb_worlds = multiworld.get_game_worlds("Pokemon Red and Blue") + + for world in pokemon_rb_worlds: + if not (world.options.type_chart_seed.value.isdigit() or world.options.type_chart_seed.value == "random"): + seed_groups[world.options.type_chart_seed.value] = seed_groups.get(world.options.type_chart_seed.value, + []) + [world] + + copy_chart_worlds = {} + + for worlds in seed_groups.values(): + chosen_world = multiworld.random.choice(worlds) + for world in worlds: + if world is not chosen_world: + copy_chart_worlds[world.player] = chosen_world + + for world in pokemon_rb_worlds: + if world.player in copy_chart_worlds: + continue + tc_random = world.random + if world.options.type_chart_seed.value.isdigit(): + tc_random = random.Random() + tc_random.seed(int(world.options.type_chart_seed.value)) + + if world.options.randomize_type_chart == "vanilla": + chart = deepcopy(poke_data.type_chart) + elif world.options.randomize_type_chart == "randomize": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + tc_random.shuffle(matchups) + immunities = world.options.immunity_matchups.value + super_effectives = world.options.super_effective_matchups.value + not_very_effectives = world.options.not_very_effective_matchups.value + normals = world.options.normal_matchups.value + while super_effectives + not_very_effectives + normals < 225 - immunities: + if super_effectives == not_very_effectives == normals == 0: + super_effectives = 225 + not_very_effectives = 225 + normals = 225 + else: + super_effectives += world.options.super_effective_matchups.value + not_very_effectives += world.options.not_very_effective_matchups.value + normals += world.options.normal_matchups.value + if super_effectives + not_very_effectives + normals > 225 - immunities: + total = super_effectives + not_very_effectives + normals + excess = total - (225 - immunities) + subtract_amounts = ( + int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * normals)) + super_effectives -= subtract_amounts[0] + not_very_effectives -= subtract_amounts[1] + normals -= subtract_amounts[2] + while super_effectives + not_very_effectives + normals > 225 - immunities: + r = tc_random.randint(0, 2) + if r == 0 and super_effectives: + super_effectives -= 1 + elif r == 1 and not_very_effectives: + not_very_effectives -= 1 + elif normals: + normals -= 1 + chart = [] + for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], + [0, 10, 20, 5]): + for _ in range(matchup_list): + matchup = matchups.pop() + matchup.append(matchup_value) + chart.append(matchup) + elif world.options.randomize_type_chart == "chaos": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + chart = [] + values = list(range(21)) + tc_random.shuffle(matchups) + tc_random.shuffle(values) + for matchup in matchups: + value = values.pop(0) + values.append(value) + matchup.append(value) + chart.append(matchup) + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + world.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + + for player in copy_chart_worlds: + multiworld.worlds[player].type_chart = copy_chart_worlds[player].type_chart + def generate_early(self): def encode_name(name, t): try: @@ -126,33 +228,33 @@ def encode_name(name, t): return encode_text(name, length=8, whitespace="@", safety=True) except KeyError as e: raise KeyError(f"Invalid character(s) in {t} name for player {self.multiworld.player_name[self.player]}") from e - if self.multiworld.trainer_name[self.player] == "choose_in_game": + if self.options.trainer_name == "choose_in_game": self.trainer_name = "choose_in_game" else: - self.trainer_name = encode_name(self.multiworld.trainer_name[self.player].value, "Player") - if self.multiworld.rival_name[self.player] == "choose_in_game": + self.trainer_name = encode_name(self.options.trainer_name.value, "Player") + if self.options.rival_name == "choose_in_game": self.rival_name = "choose_in_game" else: - self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival") + self.rival_name = encode_name(self.options.rival_name.value, "Rival") - if not self.multiworld.badgesanity[self.player]: - self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] + if not self.options.badgesanity: + self.options.non_local_items.value -= self.item_name_groups["Badges"] - if self.multiworld.key_items_only[self.player]: - self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off") - self.multiworld.dexsanity[self.player].value = 0 - self.multiworld.randomize_hidden_items[self.player] = \ - self.multiworld.randomize_hidden_items[self.player].from_text("off") + if self.options.key_items_only: + self.options.trainersanity.value = 0 + self.options.dexsanity.value = 0 + self.options.randomize_hidden_items = \ + self.options.randomize_hidden_items.from_text("off") - if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2: + if self.options.badges_needed_for_hm_moves.value >= 2: badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3: + if self.options.badges_needed_for_hm_moves.value == 3: badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"] - self.multiworld.random.shuffle(badges) + self.random.shuffle(badges) badges_to_add += [badges.pop(), badges.pop()] hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] - self.multiworld.random.shuffle(hm_moves) + self.random.shuffle(hm_moves) self.extra_badges = {} for badge in badges_to_add: self.extra_badges[hm_moves.pop()] = badge @@ -160,79 +262,17 @@ def encode_name(name, t): process_move_data(self) process_pokemon_data(self) - if self.multiworld.randomize_type_chart[self.player] == "vanilla": - chart = deepcopy(poke_data.type_chart) - elif self.multiworld.randomize_type_chart[self.player] == "randomize": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - self.multiworld.random.shuffle(matchups) - immunities = self.multiworld.immunity_matchups[self.player].value - super_effectives = self.multiworld.super_effective_matchups[self.player].value - not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value - normals = self.multiworld.normal_matchups[self.player].value - while super_effectives + not_very_effectives + normals < 225 - immunities: - if super_effectives == not_very_effectives == normals == 0: - super_effectives = 225 - not_very_effectives = 225 - normals = 225 - else: - super_effectives += self.multiworld.super_effective_matchups[self.player].value - not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value - normals += self.multiworld.normal_matchups[self.player].value - if super_effectives + not_very_effectives + normals > 225 - immunities: - total = super_effectives + not_very_effectives + normals - excess = total - (225 - immunities) - subtract_amounts = ( - int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * normals)) - super_effectives -= subtract_amounts[0] - not_very_effectives -= subtract_amounts[1] - normals -= subtract_amounts[2] - while super_effectives + not_very_effectives + normals > 225 - immunities: - r = self.multiworld.random.randint(0, 2) - if r == 0 and super_effectives: - super_effectives -= 1 - elif r == 1 and not_very_effectives: - not_very_effectives -= 1 - elif normals: - normals -= 1 - chart = [] - for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], - [0, 10, 20, 5]): - for _ in range(matchup_list): - matchup = matchups.pop() - matchup.append(matchup_value) - chart.append(matchup) - elif self.multiworld.randomize_type_chart[self.player] == "chaos": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - chart = [] - values = list(range(21)) - self.multiworld.random.shuffle(matchups) - self.multiworld.random.shuffle(values) - for matchup in matchups: - value = values.pop(0) - values.append(value) - matchup.append(value) - chart.append(matchup) - # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" - # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to - # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes - # to the way effectiveness messages are generated. - self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) - self.dexsanity_table = [ - *(True for _ in range(round(self.multiworld.dexsanity[self.player].value * 1.51))), - *(False for _ in range(151 - round(self.multiworld.dexsanity[self.player].value * 1.51))) + *(True for _ in range(round(self.options.dexsanity.value))), + *(False for _ in range(151 - round(self.options.dexsanity.value))) + ] + self.random.shuffle(self.dexsanity_table) + + self.trainersanity_table = [ + *(True for _ in range(self.options.trainersanity.value)), + *(False for _ in range(317 - self.options.trainersanity.value)) ] - self.multiworld.random.shuffle(self.dexsanity_table) + self.random.shuffle(self.trainersanity_table) def create_items(self): self.multiworld.itempool += self.item_pool @@ -275,9 +315,9 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)] def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations): - if not self.multiworld.badgesanity[self.player]: + if not self.options.badgesanity: # Door Shuffle options besides Simple place badges during door shuffling - if self.multiworld.door_shuffle[self.player] in ("off", "simple"): + if self.options.door_shuffle in ("off", "simple"): badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player] for badge in badges: self.multiworld.itempool.remove(badge) @@ -297,8 +337,8 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations for mon in poke_data.pokemon_data.keys(): state.collect(self.create_item(mon), True) state.sweep_for_advancements() - self.multiworld.random.shuffle(badges) - self.multiworld.random.shuffle(badgelocs) + self.random.shuffle(badges) + self.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) @@ -318,7 +358,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations raise FillError(f"Failed to place badges for player {self.player}") verify_hm_moves(self.multiworld, self, self.player) - if self.multiworld.key_items_only[self.player]: + if self.options.key_items_only: return tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player == @@ -340,7 +380,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)] if not learnable_tms: learnable_tms = tms - tm = self.multiworld.random.choice(learnable_tms) + tm = self.random.choice(learnable_tms) loc.place_locked_item(tm) fill_locations.remove(loc) @@ -370,9 +410,9 @@ def pre_fill(self) -> None: if not all_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) - if self.multiworld.old_man[self.player] == "early_parcel": + if self.options.old_man == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: for i, mon in enumerate(poke_data.pokemon_data): if self.dexsanity_table[i]: location = self.multiworld.get_location(f"Pokedex - {mon}", self.player) @@ -384,13 +424,13 @@ def pre_fill(self) -> None: locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: rule = None - if self.multiworld.fossil_check_item_types[self.player] == "key_items": + if self.options.fossil_check_item_types == "key_items": rule = lambda i: i.advancement - elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": + elif self.options.fossil_check_item_types == "unique_items": rule = lambda i: i.name in item_groups["Unique"] - elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": + elif self.options.fossil_check_item_types == "no_key_items": rule = lambda i: not i.advancement if rule: for loc in locs: @@ -406,16 +446,16 @@ def pre_fill(self) -> None: if loc.item is None: locs.add(loc) - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player) if loc.item is None: locs.add(loc) for loc in sorted(locs): - if loc.name in self.multiworld.priority_locations[self.player].value: + if loc.name in self.options.priority_locations.value: add_item_rule(loc, lambda i: i.advancement) add_item_rule(loc, lambda i: i.player == self.player) - if self.multiworld.old_man[self.player] == "early_parcel" and loc.name != "Player's House 2F - Player's PC": + if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC": add_item_rule(loc, lambda i: i.name != "Oak's Parcel") self.local_locs = locs @@ -440,10 +480,10 @@ def pre_fill(self) -> None: else: region_mons.add(location.item.name) - self.multiworld.elite_four_pokedex_condition[self.player].total = \ - int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value) + self.options.elite_four_pokedex_condition.total = \ + int((len(reachable_mons) / 100) * self.options.elite_four_pokedex_condition.value) - if self.multiworld.accessibility[self.player] == "full": + if self.options.accessibility == "full": balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]] traps = [self.create_item(trap) for trap in item_groups["Traps"]] locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in @@ -469,7 +509,7 @@ def pre_fill(self) -> None: else: break else: - self.multiworld.random.shuffle(traps) + self.random.shuffle(traps) for trap in traps: try: self.multiworld.itempool.remove(trap) @@ -497,22 +537,22 @@ def stage_post_fill(cls, multiworld): found_mons.add(key) def create_regions(self): - if (self.multiworld.old_man[self.player] == "vanilla" or - self.multiworld.door_shuffle[self.player] in ("full", "insanity")): - fly_map_codes = self.multiworld.random.sample(range(2, 11), 2) - elif (self.multiworld.door_shuffle[self.player] == "simple" or - self.multiworld.route_3_condition[self.player] == "boulder_badge" or - (self.multiworld.route_3_condition[self.player] == "any_badge" and - self.multiworld.badgesanity[self.player])): - fly_map_codes = self.multiworld.random.sample(range(3, 11), 2) + if (self.options.old_man == "vanilla" or + self.options.door_shuffle in ("full", "insanity")): + fly_map_codes = self.random.sample(range(2, 11), 2) + elif (self.options.door_shuffle == "simple" or + self.options.route_3_condition == "boulder_badge" or + (self.options.route_3_condition == "any_badge" and + self.options.badgesanity)): + fly_map_codes = self.random.sample(range(3, 11), 2) else: - fly_map_codes = self.multiworld.random.sample([4, 6, 7, 8, 9, 10], 2) - if self.multiworld.free_fly_location[self.player]: + fly_map_codes = self.random.sample([4, 6, 7, 8, 9, 10], 2) + if self.options.free_fly_location: fly_map_code = fly_map_codes[0] else: fly_map_code = 0 - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: town_map_fly_map_code = fly_map_codes[1] else: town_map_fly_map_code = 0 @@ -528,7 +568,7 @@ def create_regions(self): self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self.multiworld, self, self.player) def create_item(self, name: str) -> Item: return PokemonRBItem(name, self.player) @@ -548,19 +588,19 @@ def modify_multidata(self, multidata: dict): multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] def write_spoiler_header(self, spoiler_handle: TextIO): - spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Pokemon: {self.multiworld.elite_four_pokedex_condition[self.player].total}\n") - if self.multiworld.free_fly_location[self.player]: + spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.options.cerulean_cave_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Key Items: {self.options.elite_four_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Pokemon: {self.options.elite_four_pokedex_condition.total}\n") + if self.options.free_fly_location: spoiler_handle.write(f"Free Fly Location: {self.fly_map}\n") - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: spoiler_handle.write(f"Town Map Fly Location: {self.town_map_fly_map}\n") if self.extra_badges: for hm_move, badge in self.extra_badges.items(): spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n") def write_spoiler(self, spoiler_handle): - if self.multiworld.randomize_type_chart[self.player].value: + if self.options.randomize_type_chart: spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n") for matchup in self.type_chart: spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") @@ -571,39 +611,39 @@ def write_spoiler(self, spoiler_handle): spoiler_handle.write(location.name + ": " + location.item.name + "\n") def get_filler_item_name(self) -> str: - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value - + self.multiworld.sleep_trap_weight[self.player].value) + combined_traps = (self.options.poison_trap_weight.value + + self.options.fire_trap_weight.value + + self.options.paralyze_trap_weight.value + + self.options.ice_trap_weight.value + + self.options.sleep_trap_weight.value) if (combined_traps > 0 and - self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value): + self.random.randint(1, 100) <= self.options.trap_percentage.value): return self.select_trap() banned_items = item_groups["Unique"] - if (((not self.multiworld.tea[self.player]) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) - and (not self.multiworld.door_shuffle[self.player])): + if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) + and (not self.options.door_shuffle)): # under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being # able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything banned_items.append("Poke Doll") - if not self.multiworld.tea[self.player]: + if not self.options.tea: banned_items += item_groups["Vending Machine Drinks"] - return self.multiworld.random.choice([item for item in item_table if item_table[item].id and item_table[ + return self.random.choice([item for item in item_table if item_table[item].id and item_table[ item].classification == ItemClassification.filler and item not in banned_items]) def select_trap(self): if self.traps is None: self.traps = [] - self.traps += ["Poison Trap"] * self.multiworld.poison_trap_weight[self.player].value - self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value - self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value - self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value - self.traps += ["Sleep Trap"] * self.multiworld.sleep_trap_weight[self.player].value - return self.multiworld.random.choice(self.traps) + self.traps += ["Poison Trap"] * self.options.poison_trap_weight.value + self.traps += ["Fire Trap"] * self.options.fire_trap_weight.value + self.traps += ["Paralyze Trap"] * self.options.paralyze_trap_weight.value + self.traps += ["Ice Trap"] * self.options.ice_trap_weight.value + self.traps += ["Sleep Trap"] * self.options.sleep_trap_weight.value + return self.random.choice(self.traps) def extend_hint_information(self, hint_data): - if self.multiworld.dexsanity[self.player] or self.multiworld.door_shuffle[self.player]: + if self.options.dexsanity or self.options.door_shuffle: hint_data[self.player] = {} - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()} for loc in location_data: if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]: @@ -616,57 +656,59 @@ def extend_hint_information(self, hint_data): hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\ ", ".join(mon_locations[mon]) - if self.multiworld.door_shuffle[self.player]: + if self.options.door_shuffle: for location in self.multiworld.get_locations(self.player): if location.parent_region.entrance_hint and location.address: hint_data[self.player][location.address] = location.parent_region.entrance_hint def fill_slot_data(self) -> dict: - return { - "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, - "require_item_finder": self.multiworld.require_item_finder[self.player].value, - "randomize_hidden_items": self.multiworld.randomize_hidden_items[self.player].value, - "badges_needed_for_hm_moves": self.multiworld.badges_needed_for_hm_moves[self.player].value, - "oaks_aide_rt_2": self.multiworld.oaks_aide_rt_2[self.player].value, - "oaks_aide_rt_11": self.multiworld.oaks_aide_rt_11[self.player].value, - "oaks_aide_rt_15": self.multiworld.oaks_aide_rt_15[self.player].value, - "extra_key_items": self.multiworld.extra_key_items[self.player].value, - "extra_strength_boulders": self.multiworld.extra_strength_boulders[self.player].value, - "tea": self.multiworld.tea[self.player].value, - "old_man": self.multiworld.old_man[self.player].value, - "elite_four_badges_condition": self.multiworld.elite_four_badges_condition[self.player].value, - "elite_four_key_items_condition": self.multiworld.elite_four_key_items_condition[self.player].total, - "elite_four_pokedex_condition": self.multiworld.elite_four_pokedex_condition[self.player].total, - "victory_road_condition": self.multiworld.victory_road_condition[self.player].value, - "route_22_gate_condition": self.multiworld.route_22_gate_condition[self.player].value, - "route_3_condition": self.multiworld.route_3_condition[self.player].value, - "robbed_house_officer": self.multiworld.robbed_house_officer[self.player].value, - "viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value, - "cerulean_cave_badges_condition": self.multiworld.cerulean_cave_badges_condition[self.player].value, - "cerulean_cave_key_items_condition": self.multiworld.cerulean_cave_key_items_condition[self.player].total, + ret = { + "second_fossil_check_condition": self.options.second_fossil_check_condition.value, + "require_item_finder": self.options.require_item_finder.value, + "randomize_hidden_items": self.options.randomize_hidden_items.value, + "badges_needed_for_hm_moves": self.options.badges_needed_for_hm_moves.value, + "oaks_aide_rt_2": self.options.oaks_aide_rt_2.value, + "oaks_aide_rt_11": self.options.oaks_aide_rt_11.value, + "oaks_aide_rt_15": self.options.oaks_aide_rt_15.value, + "extra_key_items": self.options.extra_key_items.value, + "extra_strength_boulders": self.options.extra_strength_boulders.value, + "tea": self.options.tea.value, + "old_man": self.options.old_man.value, + "elite_four_badges_condition": self.options.elite_four_badges_condition.value, + "elite_four_key_items_condition": self.options.elite_four_key_items_condition.total, + "elite_four_pokedex_condition": self.options.elite_four_pokedex_condition.total, + "victory_road_condition": self.options.victory_road_condition.value, + "route_22_gate_condition": self.options.route_22_gate_condition.value, + "route_3_condition": self.options.route_3_condition.value, + "robbed_house_officer": self.options.robbed_house_officer.value, + "viridian_gym_condition": self.options.viridian_gym_condition.value, + "cerulean_cave_badges_condition": self.options.cerulean_cave_badges_condition.value, + "cerulean_cave_key_items_condition": self.options.cerulean_cave_key_items_condition.total, "free_fly_map": self.fly_map_code, "town_map_fly_map": self.town_map_fly_map_code, "extra_badges": self.extra_badges, - "type_chart": self.type_chart, - "randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value, - "trainersanity": self.multiworld.trainersanity[self.player].value, - "death_link": self.multiworld.death_link[self.player].value, - "prizesanity": self.multiworld.prizesanity[self.player].value, - "key_items_only": self.multiworld.key_items_only[self.player].value, - "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value, - "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value, - "stonesanity": self.multiworld.stonesanity[self.player].value, - "door_shuffle": self.multiworld.door_shuffle[self.player].value, - "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value, - "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, + "randomize_pokedex": self.options.randomize_pokedex.value, + "trainersanity": self.options.trainersanity.value, + "death_link": self.options.death_link.value, + "prizesanity": self.options.prizesanity.value, + "key_items_only": self.options.key_items_only.value, + "poke_doll_skip": self.options.poke_doll_skip.value, + "bicycle_gate_skips": self.options.bicycle_gate_skips.value, + "stonesanity": self.options.stonesanity.value, + "door_shuffle": self.options.door_shuffle.value, + "warp_tile_shuffle": self.options.warp_tile_shuffle.value, + "dark_rock_tunnel_logic": self.options.dark_rock_tunnel_logic.value, + "split_card_key": self.options.split_card_key.value, + "all_elevators_locked": self.options.all_elevators_locked.value, + "require_pokedex": self.options.require_pokedex.value, + "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value, + "blind_trainers": self.options.blind_trainers.value, } + if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit(): + ret["type_chart"] = self.type_chart + return ret class PokemonRBItem(Item): game = "Pokemon Red and Blue" diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 0f65564a737be4e77c18cf94336ac9ba859345a9..bcd94c632d2cf9e0d3c44e142f1635cf6e42ac0b 100644 GIT binary patch literal 47245 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 K!i0yNB#b!4-%|wu literal 46356 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

%ff+)8u7 zen9!24u;67zm@qohjTF1EOFhr96#m14DLK1jxF>{S`5c=0BNu$=~%5yu46}tb&I}y z`{IGunS5cq`o)=wv97?h1A4D|{g>CAlBI79hXzp10m8nOiwYCB2>LKJ^{EGJ^Z1E> z?d0Z6HDm^JJ^AaY+RhC2H&?X;@N=E7w37_138$4h-&r6w%`%8LqSP9nsO6JK?_|@K zcg2L)`g`1KyU~xxKZw7p7m8Pqjt@S{&@3r-;Nb)XXGV%)rV|YS6U0S3<0Pg*eGm9! zr&+ONkXtzg#dhN*MzRZAaDl{(lj+siPG^IZh73Jfw0*j-%mNO#Jh+>DwUp*)dvd*k zRcS)6)xuv_AoP>sH$0;IOk$ON2aFnyFw7a^9rn%#n&ve^Hf@Tq)r`Z5mZbWX+9c#=`*V@3rGLaDry#LZF? zj?KEIQyi#>h6k)Nlol|#M&;?9ASL)zR<#;hNpi09)ii~WpYlJ6A}XqQ93bJ=R$ugl zuQY7Os@PlH96uI% zV<$hDtb4?YlC?U1aSYOd^2dfTi~vhf^@(sB5p@t?8tY5Ms$5C!&{Xvx;$}JNB!#@Z zyX(x8pBhPs%1%H_#9(}R6g8L^C)uJ_H8!pa3%9sRU0q=p*f2_w)>P6@LJKkZW|nJ; z>6%RXj>9&oLu%i0q+Qeqg+OIUFh-%| z=ipCC8%jf^>%`VdM$`yNEeHtRO@vkUkr=b?bTq{?Wi zLY-B85QrwJkePW5CnzA*?rI^#{9u~^1HFC?Eowhp$|rxR@sqm=YREVFh>Cw!smdrM z^MO5YI+Cbk=ryGe)v!LCTpk;4rb1GD={J81yk!?|L^|V*DwoW$pZ7jQm`{^LCiBe9 zR-gvAw(EvUq*8K_E9V1iyFJD(9m%VC5|XfP`a8JhGNB1f(Xc(X{>8seNqq#O9+^HB z9mm^W3B_}o1~(#+HIiFeY9*Kf&C6pkD1BlnzToMaWxzHwW73la$jsU~QxXW^hqVz| zr6cqatm^OD#9nr`&oj}SRDW>{HUP>YRwMWRr99eFdLe@Z3LG#UZBDlst_tY^7^JYW zf{Ta&U&|ua6j2*r+mnkxngu~=h}FY0-*#UC$t{>{UzvwC&6#zD-KLofHuVU=Wc|p&bzrz8)n6HDdEN zC#h3+00vReAoT+}@HG91W(8u|_rGsS?aa$vIsGR9*NtB_5)Rxdt5Dx8nWY)25tH1{ z+EsIdz15h}X3ChV!e~$RWzaE5`UaVQq}58`5fd@N)rZreBVozbMFnyZTMCZ+$ekJe z-0^skF2pkBn)5_*Tkxrnx0X0YPRD)}slUR7>pOH9g9uKIJwC0h2>gj5kVlD59&WIA zd{l|*!-TcoZNc<_WNXH7a1V%P$^2-;`O%}_UQV$>@y1r4nZ7v6EJUlXuX>QK+QjTSB^9RsFfh1G*2^oU~9&HGrWyM(h*ZNKV`sSOsij_RKN zz~>jhI_{{Y?#L0ot|4+it$$e9=2$w~*c{@D^j8+VkMiPDTn5{4Xjy_7H5?bJu;(_X zt$nex$%z{nmVQYmi(|{_s#G!N!U|z@<|C}iQSKYfL#uY~hiT632UZB zPTzt?xZQdq-kpY%grK7fEAqxavYjLImO2|-nsyBC4c{D~chzzA9dxcso* zE(D)6mZrpgHy(}HS`UfCg4Z3=#uK?=gBntxq00a?ltDS6YoHEubMK!5VH%a{Sru{O zZpLJ%8yW2raXO+E(R;Xsux1SRnH7$v8}Pwcj6~ec`o;i5Yg84Udt^o-d5tiz)_6o4S~@eEvN<*@381R3($#%!lSHV}r`ycRX)U+HiI4bB z#P49;YefYUCIwh7`Hlw;Zog7$P87c#sU|S~Dw;D(K?qU!;WtfVQDqnwDNNnKCCY9z zqLsm^v1o?F7>%l@Objai@4cfpQ_GVDr^_?Thm9*WVQSN;^TWFoY}RuTb`0C_hf)%xj$kp*LoV+m^f-H;Zw6~C zPAX)3sg#hWf$4&R!_p-n`2ik!=!7{8vFer@IF`u~ZPFB3bU!MStf<{^3>2iKk~eEi ztKo_Kmtsh|VD^8=Q>QkaGAk{`Z0L1Sd}oaNNkS$bY=H6+<>h@hu$u4BKHAfaV*^+| z*Iy^K=eGy*-%h^XZJg&T>-s`1R-55tHIyhdIg-&G3Owb@+D`+Vcy9l#rW^YW+~ZyewOFS(~JrWT>^Hfg*x4m;Eh5H77@jf_Z30Lq(hM>pR$nvt*%J@q?uj&yfb;L`&ag3vU@4&CI~4W)kw z9(U#YqsPD8s`jV9psNB}@vx{y7m0ktQXk8Qv`;&{?BMOhPT~?VXcQr_#;yLOcJ1nG4Uxb8VGB-0(cqE)fZWrpZD>&V`Uz@ zEW+!)@+&K+fQ7^7R|zMZatoQ(DyaI7>2ISjiBw&u)p(%X@A6KB6U&&x2j9-~%#@3( z`uv`C@q(u0yIpBL|9f9*gKf#RAH#nszR3!IN$$P=qafz6U48r~k9OwXZ+fs``|T5V z-=)Adzg^pfy^f9!|Jrp^Yj>4Ht?uw$&-5p=t*_tC&j0j&>Uw>pWZ-RSX<_Y6`;qpC zBfIpxzChyt0AN6$zlHZYd7b{ke&U6`6sTC3d%IDrw%kkK+GcBG>n?U6Y)dnE&iKIJ zm~Te&JwUekvKv%;m^0{5Fm3Pux2^H-={vRveU%%r_Yv&5V~Y8Kcev(PR$3XkXxDjd zizY(d_K@ISV}soxc#ww``jw~K&Tsdnc_y(CCgs3#0jMy3Wh@PKJ{$CKCHDL5NG`yD z&Im)?PV*D#zx3{WI1cG3Yd{n5D56q3ju2TN6II>iqvzfg`ebhV>%0NNb`d~vYPU>j z=hYp5gH1H7qCI@WHcb57p=g`M=rNSux)dzeT8G0Djn`+Rb+h8YaXhqsxqzz;)t13 zR|n2oM#VU)CzoiEjaeNr(_QS?mP=n+vsl5?gsTR0J53LZ? zK~}`+sY3g;Qi_?&E{|x{&CW3~T zLJ?I=`9;q#(A-l5XG_!5C}5$3Ql?@+%u>DyqN@?iP^1G-0Me=h)kn1PXgr8cWTffY z5`sb^4C%s8X|Vdo?18Ld@*1To{$@6Skl6k@LUgeBczH+8pDJ*2XJ~22E|W%tC`}}) z6(PIF?L=$t&5 z9u=P5W3ct!PKHceEETKv96D1$FcaRLIL~(iYfA`0Fko8Ve3VWUL~yt=Dc#3pMBGKG z*$Oab*Fp~12C6pS+BU1&aZ0-_?gBV1)1%1hV}%}}yb z`j|3^N*_K6!QsIAJ|xCY>V;neb-34#c;oMk0;VL$fI$QSO_<%5oEEzP1z1UK;&#|_AuA-c!qKPD-J~4Xufav|$J8M%~aOd@MWyI6!o)du8 z?IUf=IAnEpB1Mg4QJ6&YLRP8@xpLU`vJz9NAeF79jBKh0R3AplQSp{OD>AMs%^=#6 zaWmuC)t{ZGW5&!PUN~r|0*4|~i+SP9&sB6Cy=tv(8S~_^eBT40X6sz$u=y=+$*`VY z1~g8V1EhkjGL!_$j2#pN!0qfiuo>sWh?W&mj9^v}D`_F#ovIF;4NG$DoS`AlfCrw3 zH0aTQW!n_yfE&>b=p6_P*FZTWP~2j1{m>~L(2(5RECJi!RmY_JEgF*?0OhbbauDr8 z3UeT*)QOa$Hm8^!K$RXvHXw%=ho@i!@W`Ta?*=v2RuMi5$dQKbYvGAxTIgkg?apnt z@Y)&%P;1()QI}^X5}x#DiPo_&b~6Z!=H|$h{zbFP;bL>X>py+>UOQEUa{XL&{8gpT z?{7R^oS2FIwz_`psHT{}$u{6)*1~UjV&41i7Sz!)D0VKPIyO__{3_(4u7uJoeGJQb zLxb17U~NQrfR02^;7y4on>8ReEqg>c%%vhg6(r_t2nvQORlj?DJ?Z7Zm%Pj9(~EHa zFQwaLxV#=b+&me=y^A#k@L4(5p>jtU4e2ek*Ua#x9>Yj0pq#pkPV4L7=f;uGA#nun zIB*F$j>hvk4mI$_Ryx)LV;zF=?_AE3^4q&$w1c(ZYEyP>!T(tim%(t)waIKsw4Xk%X`+x6byt5M%(NWFQ1V zVL{P~Qb`0;_t=hEBp@F1n5RcZg-~o-AgBTfkP~lfWwkwv0K4wU7>(brlb29aZcFOn zbQ5N6&8Vezu+i#vvmTbXz?vl3dRu?J&dr7-(c@ky7pZu)Bo7{~F?Cg4EV<%YO|3aa zGSsVzg%txQ62uiB#{mGiytK-xRShh??(eYL>?fC3^6BpVhr(O_yy@;N92m*iz%_^{ zf#q;%kO30-uJjw_qv~POm?!UEW3-2**15rOltGf9oevwp^R6$MaMMu`{|{|KfO?35 zzxA{lUDYTl{PT+wW<=nU}3DjLwQpXw*kpP1GA zN-J|12R`HbR9uJlIw(IHLY5!DZ(Q#`imloF)pW&~6Y z97PJxhQ!aGweav}!B~0mJ$B!h<@+1*L<8HA0MUvD7zeY7qYZeudwT3H;@J(oUaJ+x zzIu7bREkJ?mwRn-|6Oh^s=t43)E=#ySrR1|fk35iz~Ti5MaBh% z|LT5R_LTw|g=?cEQ0L(&pn3oKCT#zv@{3VI4Bt+Gp-q<~xwzKHp^EHVp4!8XA$&P- z+mA*CVasK;P+c8Lt@I5T7&bESF3YJx4+bEu*z(}3gi^oHUmeEeI20~)Qq=s-F~NbC z8I|=PYOG%Bj`5&ZHK0X*=Nf8GnhHMe^3!Fgw(wOSP}%fQq)2Ix`ac?E$&py6B7y)` zWaWa94phB06$ljsaMBCN>yT4h(os70EQ+|g5PwLZY)YK6CzL44 zv0%HhXV_3Mkrl+v@fIL|85 z>-(uu0Rm8Oq0>Nw4qDS5BY0;%mD*8vzw0p}xh?$ejaJFf)|AZ}ZINPw!0)nR=~a_G zl$wSojj@z$Gjeb0m4*dN4p!K0Hb8^m9OnrdKlrwq;!&s!R|>M(y1U7g6;NDaX;{;9 zR>=5}eL3Bqw*uOCU(TgCrQkl_<5I+!Nv*|2TezNA>@u2PZ!TQ$ghd9Er*@MFS;AJd zfJ>Pbm-!2F<<9Jnj#_c9L)cPsCospuvOoOf#aHIY9~_QWlt>JN8i~N%-La8z44(2( zfy73wi@`9Wf(ctY-V>mzrR~=0&ZlLYrMj4t(w9ZR<( z1Og-?1acxnvZ5kQ7-cdjfS?NrBEx%81X530C^noJd9gp{1_^XQJ~{|npG9G#919Wx zV@mjXEomH>bo8qHdX_|XjkCsJ#S*&8z0GXc5@2v$hU=VKJgl9T-RA>^ejyV)6w3(! z9u#i>Y83An2i>IfHdCQju9vR-{C|s%&YDe~o{lHry$pw<$p@nq4Ja34piLNwA)0%L zZ>Ks4_HZgD0{+zqYb`R?XH8LjYs8nI*pkb5=KcFs*F)%5LO^B4R~igt9@Ra z7$WMBtw~Y}En>@TsIg=fV#q2i7@)9HECp47oYX4?kz!4UI@u2~gwYM}Gw%L9J_>U` zL$k+&Z$5Dj?Y1aoi|t}JU3rO=bHxA|)n5Ag0D(~u4*NgSgR~!{Sh9>k<(c0pc zv0m*(r!rh<5|B>0048RyO~u=M&8O#4Iy74noj)#2``rd$Nwq@H)tPWWE79lNGvTMU z-Iw&Ul=n=Uak9FfyC$WHVXIJ{YCw{D&4~uF|an-%0YE8 zZ%&OF=Ndk2M)>fu(g$mCLuO{hOVqlMFD@bSQ@NQZi9ER}`xKQk83W6UyOU?6z= z-nH5Z`5-*ZR_KM8N8l7do=HR*c5U(8S6>sJ{jVb3AH>!g zveqe(rcy8vJo~$g0qS5k?bEjqEyLez+U<_mq0%6xE~X57`B&5o@n40B@$+U6XES2` zYa?_QEc6qw!LdCJ&Z(y>f8Wp;&B6ucduT5g-92tnrq@(_mK@&2-pnS!bx1msubPC3 z0OkskP%#1zVW@UoZVWJ%&I>-f5kds6KPGIV{zoZFT_-mh?vb#1Oji*vZ{w!)efCw4 zf@hLuBkAZspt1l0B?b`DhLp@hMj|jIv(D2Bt&)=8^A0j88MsgOTNVoItaws`1ol>8 z`;Jr}<`tT#_ROf2mHLXz6qEw8hvBMkVOUV8lFyd?=s=UKyWB`XCb0&+3=mY+=kcum zWPOkPWwx9rs-(E_YKVi!fEn`g_}K67Fch1ZPobK6{EIO4@SuPAlY7C{JN!b4u`lXd z__aoOkA)KiKW2NFFWXKY|37WsUvU?l>rk6|#$ESy)tPcUb044G$$4;x5&~Kehc1cg z;dcWihS0?qR(e&|iW}u6g{X;1cS&fl!Zk#*Z|x=pw+=G#b+9{Bx;OT`td=2j*hYJK zM{_PWfwGqs{MYCdZo*SU$I{L0(gb)x2K%dvsA;%t5D+Aw$-_~E^G>-G8Z#B@LR_cx zNE0Ch>x^g+Ah`psRDh;~5*6!UPjT0ll^dJ()6nQuX$}cE^wm8lQ0Zv#>bxDoT+=9M zK^YAhQdET)j}C+H==V%Ha-PG|>ro3*#6Tzmrcf~ue)2#U@osq;_G<+tpm-*z*i9Ok z?dL4GfMT@^4-=?dTP7kd!5~wVKpMm*Bl*3h zNm!;BXzn&~ayj%wiIG7>&`>>4WMU6?zixQjYMnv`S_*|&SIvkfVdsOGuk)|E%@zVE z2(X6o?TXRReM@4@90Y#Ix=?5!dkz$XB^mIwg)-~kWAX4V6F~&zXSLHzb!>FWr`X*SyWPF(*f^E`(bNTmSPBXPGDQUdQV~E6=_(>X zQbiUKuI|3C9(~Fm6Wr2nA|Lywnun_QJ9O$j{iq#GuWw4AY-$%msR%wf@BfmcOKp8- z)~36$N==HSfNN$SwnDQyQv*M2lCpTdWj$|F$)UE&kpJ)S+8JMoLZ89B%smI8_8Nzs z;k!gDUhZx)Z-SqW&vBa#UH0H%edU`wmL0df)>y&)(@>k!{Pf_MXI%Hrq^1&yB6c|} z6JXp^%0B>(L8r`7oTd^5`hDNTdcWUf*vNK2OG_qr*nP%n16F>kjP_vBy{oYgGTgh9 z3yLvKb;L8C?We@ag%Ep>^8Xgh!^!9J<8$n-S+LS*;G?aaX}*GmdI z(-mw_Cr&X{t{8eum`86$J`!uC+s9l07#Wl;t+R3$(x&$b&#g6Um$Zic-8lNz5H<`E zn>{-!9a*yPrZ`zypMOBE7{>j>)12ei>iA6nL&?2Vl%&2Do(XXRmS$W)H)67L~$$$BHyaw<@5)T8TS5ipmLtk4Q2z%ZbX$LK%!5C+Cus1THl- zDy?dA-hdE4G?7Uo5Fw4H35gH4R3IP$(qEu#rC?B$^Au8A5BMw)CKyN=WReB#(^c_a zl+~XO)iU*3R&CnDSGcf*X_9YR+$6}~zuMRmgQ36#|2hp^w2;zE?Vs19@$3?F< zp=I-Q!!dTKpXlbzi4O}8l$!cfo)ulsF^OXlI+gCOhJAJSjPIk4>|RSn_ZZAxM&oXwh$3-C}PJJN!} zVXWt79xm1n&2+{~&$Q*-*F;3~{P($#FW{X>Fs`*pftu3(+$tgJP%N;!b{yJh;}X}> z#@R5ZW9#B$XtssX5)ErY7(gHiSlVJ!X(;|E`iQ3ALMS^@EtV-%R_=OHTT}k^R12aK zR+1XbHaeor(UaEx%AfrvgR-~y&iC$9ozaB*q^ODPGmgXEdXMyujP!lv>5jZ=Rtgnl zP`as2LWnJrg(bp`?~&K19>+ILt)T@cle+tk_Tg-PkGZX@t|=lDIS3V$^2+)A-a93| z+tT)Ju?3726-X=rBN0Xd{c;rrhOJT0Zs2cM;%kx`9-}3LA1xtKR9Fik1&RWquvm(+ z5bZL9ALg{*XuVON?>#?-O*KN7UgkR(eK&`jLt-0Pv%1BaH8Q~P;)k#^{N0seKgn}F zQp{1<{OTyukW!K;q<>Qih9vB$Fksz68HS&Pt{r4Th#-8{Z(GN>@;0e3=cl&Uy1@c7 zPM=K^FT~}+sQfk`5f83p_WZY7WlF;vTl~HJOo#bz7=l3U%!;9tHcGLGrsZG;IfbdB z@j0n*!c2*?8yuFB1*A2~gnz}+x)|2yEttb9NGuR=GbLu}z3Wq>s>^dA583tjLM9Fb-#YYKIB}@xW6t zWWbV;td8iUbgzr_%3Mvl?NGf@$`K>n*E+++EGQ)lTIpMn=2D3$n#}6NlZ|hK6)p2? z4TQ;>hYS{tk-U{@DOJeB$wJbWtqc;g;E*s;Ljk@~EF%OfxH}XTmaRgIRS;>E&SypJ z5LD{Xj|+Z^WRfGm$7l>e817~qQV~&W{gqd9I1Q(mRTpYNHWp!98H{Ei1?6cgFByg~ z*CSl^;%_kZxFY7<+wrrPIiD{Ue67@w5B?Yd;C|CHMDKJh+h0jx$x=8WVSsg=^{hwd z{3^h0)$fl~y0CPAH&n2~6TqjB9dns^W8h(e)SDdYw*rvjbS%#bAGF0^O$w@Y+1g=5 z_lcB}eo_VTQ-c8TvnapjKLmbrs!i+<;J-nT+Q~N|;9?9CLdZdpnG4)bwRl^*MV=Q- z*#=}a}H<8s;Z%+yYG?j(3jCJZizS6@%rF!5F9-)Sc_+Fz-&e5j{QK|AlNH@bDs@cKAzH>2_Xx#55Nb{yHj7F?B`?gZ6PXWdwbn z@FGm@CVhhpA=qYg9KC(y7SMYoaZBhit7k{dLGwOIz`bz~6ockqPM5mZD0h=aPD<#{ zN$c?4ombFiHrd0;Z?)*2EQXYONCUIUq1e-))`_Q{jFY&A?C#-=i+ngyv+k2I_7kBz z4p9Hk_;e3p3EX@g-pwsV=0B;P8R z5i*Gq&pvDA;bLslooeJ2L&!Nwc8VlL3WB<9;h2a4_g-TB|4I(nH)HQ3tjUYY`<4^U zB#0;-O66czdB(9{X|W*W6p{Ut5dNs!zr~@py2Ag|G-(M3WxGS?dB2ro;U(X4l7Rt#v>I((@LA4I((y^N;AVg;@Qy!#Kv?4WDxN7A6x zj@keVhU008Rd|5*S`@ise4 z5Zp7zlS*!wIW;87EfM& zANT%SAf(h#`K!H!To6r6qZu4KEo-yz_}vJ0a;~|}zHX)chk5t79OBK9=6<^hjAa0k z8xk(M&xD|l8E-Zeyw3vex58voWR(jkJSgFJ9&xmw{r0+y2cAbhd6-U<6cCwMW*kAS zKK-&MH+`@u5Gb9C2k@{sk@TOBmQ9(ZmShbwmB;ItLR}UYk!_YYU4oqTVcOD*m(3Os zg_|^e)+k6&L!$ zu$IJaJtMEJr>6EHBnQ0L8t2Hsn>S0N0=+JEEbVq|Sg`^N1&jJwkefyw42MIzHY>J= zRxFc}AH~I!YEJq<;F)Be`2u?yQhm}MG&@ zN>E>AhbXqmNF}PStXKv=W>{u@Ei2mc$WkDb1U!_H)1kfmN87Ymlk5tieAp>hmuqg$ zjG^H;`W1nUVM866=$TEOzU7?zZ1mltWd2G`84t6}1MS=@K+%T>RN(#XzsP?Ul!xAC zkm8WrZFh;3l6vh(uw$xdcrnfLUcN0QP&#iON0oCknVcv?ylc>In%@UR+ni^P$V8!f3;(06MJz=-*C`zOJt(mlj1NlVP&{o4Yrcr9<(vrYjEP>f48V z52=d8l6uM9d3iQ(%&hsi{2Np|4#$aZDa4>%UD#0XU8S{fT?^5=ws=(35bp?v6(2a| zeC))KN8}a!pB!!?E*fmmR54=y-KNW_G2vnj}D|R_<^-H_>&$9m4@!yz3 zn|Q5_&?x~c_Ff(CiKx`~!d*4c*IqLM;zX9~f=?`_7zpLCfL!^`wmvMMQtN)V73jB=rq zp$Fwyb+fvi^xlAB4mhzxF8(F(>C5JF(_)`ibyrjqF$p4qlLb&D%)xNh$Ta$@ks!T> zhfD=Q82}o*WFkbi47dAsP1y)YfO%q+-!JmCP3H^F(mTqa)uvS;a4`_vLZCUMsqX3O zG~I=S=rB&Rs1sqHIs2zPi#mR7+=y z!yz0e&d~%KY;C7jIoH2rur-qom}T|0p7GTF-OS)@6wEEPlT`Q)x>*%5(^oI%JU&SX zEGd+PQUr<|6Sk9hyKK?b%ZS2o^e)6*mdI_UxcjZt&zj0?>8A9`90%Mm;^E=)wa!*> zcd$L4-(?`j>E24HV-h%yAu?2WF_5FzcJ|~Bdr;kbV{xXgx4^5np%#y%>pO+5l zwn_*cOgV5qycP8o6W4l}ak!`wPynPU`~DAov4@!yPTNlJg80pVdP);u)O>=NAgJdj z0*5(%3Gm7U4fwxP^V-8$Hk7)}W*owwY46p{%t#Ybs1nb^tP<56#xh(?kv5A>Y-zsuaa z$Exy+Q`;M>ZT>ZZv*5ktoquKmreS7&t^yDG>h1 zM36t>!1_>%WH0NtN&zV%g94H?A(ZWT}X1zc4T`BFhss`^}k)Hk+&@w zs0q;h>X$E0qh z#qPYbV)2B-)498!iBc*Za)lvdql&8<$8Q+)_YHZ-}N?7u`O*5nE;K0hx;vc0b797W=@65a;YU?+#``!jCx+}cwvE?)KzS2FdMO+B4 zb9H!{yts1>>pSE*T&~|AxarJRd6=MR^gi#$?{?j*%jve1wXai;?fWYAy!&Fim))Lz zEz)3HLd{y)%H)CHv1&HB7|875z3#&L_&2#sct zR54otJ0vL`Pa(Qi!@}vxE;=#s*~kQd7?M0f0~8rZA9v<159;<=1iDHszWXG6IDy>} zX~WZ0Pe6}&LMO+f#_?G}1l{h^Nk4qb8O$&xnE<(yj4d@v;P$Y+BTt#$C*!}*3APJ_ zED~iLQ>|N%zPD_(o&BS~Bq9>^xIoh*rvR2_mI&PucBY#K z8cxaUdHN2oY8Z(I(m?~AnDgCQ51VM?O2{Dyl8Vvv(NI9N9cGxP;bB77Pk2-Y#_Uj07NMGEvzC7SF@ztxR<5rGOE9!eI|NVB&p9E`3HW zTm*r;#jgXl`+sTOGpM9ffpbzJPAIZn&401>bqI|Lz2S*qc^$goJ%bG;0m~0RQruO8 znaw8X)iqFn{O#UlAX+GPF(^Z0MS$Ww^dzmT5x{NhN13?+iYqv?aote&GGw6OAQLus z97FNFH6F7NqXE*09niirpzB4~`Rz=`Gp-Ikng%#)AIqBwtU#y_BgNNDcTLz^r;@qj zJ6xS>vLaoq3(0+-8x8R!IChWE`8*JYMPtSHyUaH4MjgCil*B-y1>6AG@du?I;TI z{IkFD_VHdZ5CIqX^B=0&kY#_j;xTIpk-izi+T`b++)XTFYR~&?q<2WTNH5aHPPvJnvZhwTZCSG9-jo3PR%g4iDXJN~JAG!nJC$R$ki!saDr5lx&B{^ubXG=_6gp^uBzToKu@yDU z6Tkj0%W7b=H^LxW&6WJJ@TlZ5|>> zOqvFRM8>87LncN5j3WuAOcMbzVH#pGUP)&}ouk zk)XjcWP?zI0iiTv8Z>Ac27nrBo-`&h6HiS{2+adPOjFUOBTY2W(38f12+6b{$WKg$ zjT)!u8B9i+o>M(cO_8d4dTMxyw3$tl5vJ2r8iWd9014oj2m)egWF{t=CQYDeJxwQ> zG}3w#LTT!1Kh;mwKU38Y+M1`bYBZix^wZRl;ZIK~spM&>KT}Y7N0K+8Jtn4UHlrgA zC#mH#P;Ez}Q`02Ip%XzcLqJT4rjrSPY7D9R6Um`Z(q%m-=$dAv^wSeenrO&rr1d=` zMqxqesMBbTG-hpfmu`(UVOX8e%ju8352TP-rw{&}w=DB4~{So|D30fF?<% z%516SJx%Il@}|mujHjhNCy6uFJxx7EjGj}}Y5J4WJcRK_l=R92CWAqv)X)HEXa}Sj z446!S44MrC)CNN!G}0hI047Zu4NL$j;WWyhiIYV1(@hlHKqeCe(UTKRMw9X~Xw^K( z&?b*a(rK|WX{7y0={*3`QyE50rlV-eG-$#ZF*Gw%6Ve{1l&O;IEFj{)nWuE5=!`^2 zg#r?X03gZw|6<}#w)!AEIY4!dWIHx2^>iet`+C~pxZmz$LiXOI1(Hz?IsL&94Z{%w zH}b-P>R}xjJb@>IWYaoYTSXf(tq@lDaOgl%#3*k_r5UNyA85|s8ApkT-iPdZ7C%p1CU8a*w zj`o{Fp<{IM?D0v$xa(QUSBZ^(9w)EV%k?9iF$xGnd-jd9?k=_RK0F2bwIvIrr-Oz2 z66ZukE8g2TcV`zk0qMSezIv!?AKA@F4$qMa5qeOD>u z3h1J5q(B1kA_qCW#h$`mw`(v!hmvSAVG!Y+ z;Gi?;a<{S2)qFWOaN1nB$g-+JUh$jshL;$`0u2Z7sH}7pfM;dbtLa3ziJHq;oDlJ_ zhJmNoC=e=t>C`*9-P}6CA~SGm5VxI!Ier}~ck;|mhI>mSwiK$badj;N6=GPz9KHMZ zClE3TMRdEgL<3JH$G9ai&k`bp8@VVS3(L5U$0G+`685-EnWj+`p%{I8;Q|IN!>dj0 zAEBmA_U;Uv^4fz;>R!TKxdJ8~_b{Ov_CsLo)Mlav$uXP>^SUp;eR_DwF=m z)4B+&TEYPHVto(y6060L2x(?9nws`#=0mZwN^H5kuV~h|0=HAp?2Pl4*0`78DJ~Fz zG(w_muWuwkKsoS$2tZ(nLJWPbldhJQc0e?Tkaa{L?Rq)6KBli#cN+q4QIzKY8D&f; z1tO;bGYAa>j9UN&B;HJ$TM^f|n&x#95zkN_i5@&Y_i4678d@2N|Cu3rbC{dRCgT8izW$IKaYu&|0&4(=WJ32YP;8?OGQ_J(YFAL*Yy+1*-T zQrUj_i1UgC^r+Lg`a}o=9Yh7iM6clJYjnh>_UDN8**4-UE-Bz``26nnTg20+_`nl* zD%u2ZibMe@t?I&hig;G`VgF%aF9ysE&NP9HX?8_8^?48gw?7uI z33dDNAzT%VUZ^z2bV31mboKR&ed=}$ZFU*w9%DF5;kUGE>8K`UsO!;%GZ)W~KEohK zG49z5UV-+J7%e7{KA}ESX0b&PWB{cz%tTJoq=kUfT;|)xvXQq1jKzwe8cb@kv}7g= zet`&blGV&Tvf-p40>lLXkqE?OpUpd3)dIyI?->j?d$A2vwS=XIJHfH;j~<}qt?Oj=w>fTvJiVj zr*YFH${_D;d&%Y*d!iTN&J|uqQnQ=HL{G(c93}n*>0iqsFsQCxE%hVe7D&WQ=WcD4 z!k5U_(3Rmg3{do|&!@I2jQmI3KoqyIxL1JQ*OG~48Z#dcu zdTA$7w)I|8@UeNd6Xs@8L7E@d%d~7QWJUHG6ta{z_fEmp&l*O=VMpkKy{87n@#}#I z7S=3Ka@}#dDNhEkw8HntjPeHC??1#rGSt~Ae1J56^w>a77$e?!EFde5J@Nu z9Zd%Lpkp#r3ZSW3G~*T4PQ25UZRb*uK?Z=7ISQH%8J0pyDA=kwWrYA@$YO#@0tz7{ zleB?D0Zn448!`Zd#9AaGl2R$XA&5XElq3L7vskFkp%be#L5PF|kw8s5Q@d0ok)}XH z%NofypaIA%AyAS;b}3?#2>_EZRDeuE0+0zqF;^#*I3Q3X>I`=`Q39#awS$4ZrNHwy z-U7xJtH?^S`NbL>vkT0nh{i!K?1cYLM&on(w_Xg51I>>Yzv<6+#eDj2p@c$R zOX5VHiqnCLNJ{1wv<(uTShHPU;meA z_cyfk@FIBMvO-&RXbS zJsru_1wi5U{(kCkG@%P0kHy5V(D%KsqoFm%C9@QfjnIP?f%l{ z{jw2cz81?pjLnIm+F7Xwmp*r5Sb;?W@f?YDVtpC_3APpjFm>z?;5Pn_nJNb#DYPLTb;xZIP(P zt~uzt+!>u8xZHCzGUdwcsceu-|O5%va)7@2TdU)AfbmJ9*EflOgU@$2V}-E8xMN z%s=&X8J~K1I5YuZVM1t15YzDjbLrrK3VY%Np|_{jzXaYXNTwey6$E7x5rIJA7r3D} z8w%X}s&nDL{$lx+Dy_cSH(2vIo7vSva|jpfkmYBY0p}Gv>0F&!I%kB74!PN9eBGe% z2C@r)A#sX-j=bg~a7E+roPOO3KNcT=o^Ziyw_YASt6xI`VX?Bt?2en^Pr*MCg`$W` zbM$2{wQ*fxT95%^H^~9J9onC^wzA3Fo|n28QLPGd}yH1 znL&MoS+Up+Y5=7zoLdfg4N;)xlc#PH$&M;#(rJotsD|hNpUI~rOI`#Yi8>5q8~K`? z1o`q47XS`pvOBtQd>!A4O_nB9ITo{)XTYaFL+(90fMaF5kz>|at}LXHmMI*4(?G|K zRSEwMH9 zz=;lKzNbc*pOwynSrQ9V&nZwXaKyDfgj*Z7rO%hcKFyTJ({a`4?gHGR0RFU`J$5d*4F{*7^ueB}N+}9cwmU%8D1a6fKoBDcxFJZO zQ$RT;fCVR~IUIQvv&}fKzDbE*^*%vVhfk+Vxl;NS8`b?8ley*1O*}yU=oce z!L9<(CfNx!ZHFeakCX9U4=5dhV&p4HFg031a?0icy6UC-9F5)?SZ>Eb#bS)ij zuA7zbqwnLjjd{$6K2ukV#oA!cg1SVLaxNhSYurla;A>MI6UQPc>$rId$L~Ra9rt@j zHxUOM;}Avs=FHGBWPZ&-rPZhx5McpF4rfyNQ}2+uCCP!5s&aMm4p=s)SmYCx5^S~P zDBA7$MaD_oJ5Hhi7F)ptY=*pebCua0xXENZN##!?>kK`(_NQPv%6Y-q^_TP%J)F!{R_*Iqch5$$Yw$%{dt+$g0k$uP8uBFszVfRfvg3L@1I~QW+Ut zBuQs5$Yo%&l`yhYv6NYba}MF5STiVssO=2~CJ;L^qY}`n^DPEs!?d!iGzeEnMG^>p zeOcuz3bm@aFAf!y$w121qM0tIUq9C#C&E?-)hLsUEzdOsy z!Q`qUbwpuAh|O-5OG<{g`roTPZ(Fk<4C<87)2v2$3$7@4i?{-jSLdG zbQ<_*gC0FomT5*gu8C9D$gqt5YNvSI=JyZw6G8(7Ox?Rkb?Hb zY6JG~S54O~`N`I(-kIK3I6#ARi2h5t#hzlPP*D?PPzTO-2T6L)1bvpm;B2zxNz-z3 zA^<%b2zugH%k$`R8b0dkO#h=;~PQY|7Q#jQ(cJHt{DAtG*K zf>YHF$hNSWoXT&$kB?$56*6TQbW!O6$g^RmBf>kp%A>0DLU=_11R@~>5(*MfkO(MB z5)hCH01^o#l7NIHlgx&}H?znTlC{JL`3VUGwqIrJdp(s6ke>x>X~VWUvXRr4s!9NW zcaJb+AZiDL6d>>lJ6t9rgrEu&5h_(%pUD7$3PBmRJMoav?@peD@M#X9tVVOolaOSs@+>qlP&ov0RF9DK#pcbv|wUwIEkkcl^mR9Ky| zhzpPLS1Ge&h9B=SX@(Z&)n-jSJIZ+_D;TztCen6nnsw%fyl#EAo#>|`L?2xp3=g2c zS>}Cqz3c)t&=%q1?EOD&d)aC;x=CPrJqb#o&g4sq5>hP5hlx=<+VNUzwro?A(Tkc*3!8^2No>k3WdY-iW(4b zVId&ia9OY@$wnh#Wpn*HRB_Tuzq4IZs2g(HXrq-K;h`X4v_f_`1OT~i$3!|dZOT{LeGv>XrZUdz%Lk_#UrLUJ!&BiX`9_c!F!FzM*ZNb``gQj*Pik|& z$=8i^Vjsdm^H~$|?Yu5pg|L@|Da z$wkEd40-2vI{_jz-OC*B$No)zL$3EQ6y8LMm%G?edv3a*sLBFunlV_TcO8c?I|+^5 z$b=7l!s2ybRRKyOQDjGklH(DCXC40#Qc8_h>Kjf>qfJC6>isc43ez$Bd(U?E1WaHA zPBLwaKIT|OzpPtet(P0B^S)8>;o*P)XAJXDI0Cc2Wx zaZ~C*mM3bQSZ8bwH9KW6;0aI_f84N_dYdyy5E^4Xjyr(3k@o(rW@P+3u>=C1aIbKE z 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

^yJTP6oc_cjvKX=i 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 diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index 826b7bf8b4e5ff0dd0926ca876c9930eebe4eefc..4b207108cf0c6f20611233888fc279b9ce44cb93 100644 GIT binary patch literal 47212 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 hHxq+uj|F+OgQN}lYl-uTcBrIq=semM;Zc_jN literal 46344 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 DjU7nf diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 1e5c14eb99f5..6811b5926078 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -60,11 +60,12 @@ and Safari Zone. Adds 4 extra item locations to Rock Tunnel B1F * Split Card Key: Splits the Card Key into 10 different Card Keys, one for each floor of Silph Co that has locked doors. Adds 9 location checks to friendly NPCs in Silph Co. You can also choose Progressive Card Keys to always obtain the keys in order from Card Key 2F to Card Key 11F. -* Trainersanity: Adds location checks to 317 trainers. Does not include scripted trainers, most of which disappear +* Trainersanity: Adds location checks to trainers. You may choose between 0 and 317 trainersanity checks. Trainers +will be randomly selected to be given checks. Does not include scripted trainers, most of which disappear after battling them, but also includes Gym Leaders. You must talk to the trainer after defeating them to receive -your prize. Adds 317 random filler items to the item pool -* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose a percentage -of Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty +your prize. Adds random filler items to the item pool. +* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose between 0 and 151 +Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty Poké Ball icon shown in battle or in the Pokédex menu. ## Which items can be in another player's world? diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index 6d1762b0ca71..fbe4abfe4466 100644 --- a/worlds/pokemon_rb/encounters.py +++ b/worlds/pokemon_rb/encounters.py @@ -8,7 +8,7 @@ def get_encounter_slots(self): for location in encounter_slots: if isinstance(location.original_item, list): - location.original_item = location.original_item[not self.multiworld.game_version[self.player].value] + location.original_item = location.original_item[not self.options.game_version.value] return encounter_slots @@ -39,16 +39,16 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random): return mon -def process_trainer_data(self): +def process_trainer_data(world): mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.trainer_legendaries[self.player].value] + or world.options.trainer_legendaries.value] unevolved_mons = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] + or world.options.randomize_legendary_pokemon.value == 3] evolved_mons = [mon for mon in mons_list if mon not in unevolved_mons] rival_map = { - "Charmander": self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name[9:], # strip the - "Squirtle": self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name[9:], # 'Missable' - "Bulbasaur": self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name[9:], # from the name + "Charmander": world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name[9:], # strip the + "Squirtle": world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name[9:], # 'Missable' + "Bulbasaur": world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name[9:], # from the name } def add_evolutions(): @@ -60,7 +60,7 @@ def add_evolutions(): rival_map[poke_data.evolves_to[a]] = b add_evolutions() add_evolutions() - parties_objs = [location for location in self.multiworld.get_locations(self.player) + parties_objs = [location for location in world.multiworld.get_locations(world.player) if location.type == "Trainer Parties"] # Process Rival parties in order "Route 22 " is not a typo parties_objs.sort(key=lambda i: 0 if "Oak's Lab" in i.name else 1 if "Route 22 " in i.name else 2 if "Cerulean City" @@ -75,25 +75,25 @@ def add_evolutions(): for i, mon in enumerate(rival_party): if mon in ("Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard", "Squirtle", "Wartortle", "Blastoise"): - if self.multiworld.randomize_starter_pokemon[self.player]: + if world.options.randomize_starter_pokemon: rival_party[i] = rival_map[mon] - elif self.multiworld.randomize_trainer_parties[self.player]: + elif world.options.randomize_trainer_parties: if mon in rival_map: rival_party[i] = rival_map[mon] else: - new_mon = randomize_pokemon(self, mon, + new_mon = randomize_pokemon(world, mon, unevolved_mons if mon in unevolved_mons else evolved_mons, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + world.options.randomize_trainer_parties.value, + world.random) rival_map[mon] = new_mon rival_party[i] = new_mon add_evolutions() else: - if self.multiworld.randomize_trainer_parties[self.player]: + if world.options.randomize_trainer_parties: for i, mon in enumerate(party["party"]): - party["party"][i] = randomize_pokemon(self, mon, mons_list, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + party["party"][i] = randomize_pokemon(world, mon, mons_list, + world.options.randomize_trainer_parties.value, + world.random) def process_pokemon_locations(self): @@ -106,21 +106,21 @@ def process_pokemon_locations(self): placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - if self.multiworld.randomize_legendary_pokemon[self.player] == "vanilla": + or self.options.randomize_legendary_pokemon.value == 3] + if self.options.randomize_legendary_pokemon == "vanilla": for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item("Static " + slot.original_item)) - elif self.multiworld.randomize_legendary_pokemon[self.player] == "shuffle": - self.multiworld.random.shuffle(legendary_mons) + elif self.options.randomize_legendary_pokemon == "shuffle": + self.random.shuffle(legendary_mons) for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) mon = legendary_mons.pop() location.place_locked_item(self.create_item("Static " + mon)) placed_mons[mon] += 1 - elif self.multiworld.randomize_legendary_pokemon[self.player] == "static": + elif self.options.randomize_legendary_pokemon == "static": static_slots = static_slots + legendary_slots - self.multiworld.random.shuffle(static_slots) + self.random.shuffle(static_slots) static_slots.sort(key=lambda s: s.name != "Pokemon Tower 6F - Restless Soul") while legendary_slots: swap_slot = legendary_slots.pop() @@ -131,12 +131,12 @@ def process_pokemon_locations(self): location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item)) swap_slot.original_item = slot.original_item - elif self.multiworld.randomize_legendary_pokemon[self.player] == "any": + elif self.options.randomize_legendary_pokemon == "any": static_slots = static_slots + legendary_slots for slot in static_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_static_pokemon[self.player].value + randomize_type = self.options.randomize_static_pokemon.value slot_type = slot.type.split()[0] if slot_type == "Legendary": slot_type = "Static" @@ -145,7 +145,7 @@ def process_pokemon_locations(self): else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, randomize_type, - self.multiworld.random)) + self.random)) location.place_locked_item(mon) if slot_type != "Missable": placed_mons[mon.name.replace("Static ", "")] += 1 @@ -153,16 +153,16 @@ def process_pokemon_locations(self): chosen_mons = set() for slot in starter_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value + randomize_type = self.options.randomize_starter_pokemon.value slot_type = "Missable" if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) while mon.name in chosen_mons: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) chosen_mons.add(mon.name) location.place_locked_item(mon) @@ -170,10 +170,10 @@ def process_pokemon_locations(self): encounter_slots = encounter_slots_master.copy() zone_mapping = {} - if self.multiworld.randomize_wild_pokemon[self.player]: + if self.options.randomize_wild_pokemon: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - self.multiworld.random.shuffle(encounter_slots) + or self.options.randomize_legendary_pokemon.value == 3] + self.random.shuffle(encounter_slots) locations = [] for slot in encounter_slots: location = self.multiworld.get_location(slot.name, self.player) @@ -181,11 +181,11 @@ def process_pokemon_locations(self): if zone not in zone_mapping: zone_mapping[zone] = {} original_mon = slot.original_item - if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]: + if self.options.area_1_to_1_mapping and original_mon in zone_mapping[zone]: mon = zone_mapping[zone][original_mon] else: mon = randomize_pokemon(self, original_mon, mons_list, - self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) + self.options.randomize_wild_pokemon.value, self.random) # while ("Pokemon Tower 6F" in slot.name and self.multiworld.get_location("Pokemon Tower 6F - Restless Soul", self.player).item.name @@ -194,7 +194,7 @@ def process_pokemon_locations(self): # the battle is treates as the Restless Soul battle and you cannot catch it. So, prevent any wild mons # from being the same species as the Restless Soul. # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix - mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) + mon = randomize_pokemon(self, original_mon, mons_list, 2, self.random) placed_mons[mon] += 1 location.item = self.create_item(mon) location.locked = True @@ -204,28 +204,28 @@ def process_pokemon_locations(self): mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - if self.multiworld.catch_em_all[self.player] == "first_stage": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + if self.options.catch_em_all == "first_stage": mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - elif self.multiworld.catch_em_all[self.player] == "all_pokemon": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + elif self.options.catch_em_all == "all_pokemon": mons_to_add = remaining_pokemon.copy() - logic_needed_mons = max(self.multiworld.oaks_aide_rt_2[self.player].value, - self.multiworld.oaks_aide_rt_11[self.player].value, - self.multiworld.oaks_aide_rt_15[self.player].value) - if self.multiworld.accessibility[self.player] == "minimal": + logic_needed_mons = max(self.options.oaks_aide_rt_2.value, + self.options.oaks_aide_rt_11.value, + self.options.oaks_aide_rt_15.value) + if self.options.accessibility == "minimal": logic_needed_mons = 0 - self.multiworld.random.shuffle(remaining_pokemon) + self.random.shuffle(remaining_pokemon) while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0]) + len(mons_to_add) < logic_needed_mons): mons_to_add.append(remaining_pokemon.pop()) for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = encounter_slots_master.copy() - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_base_stats", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base)) - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_types", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], poke_data.pokemon_data[slot.original_item]["type2"] in @@ -233,12 +233,12 @@ def process_pokemon_locations(self): candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] for location in candidate_locations: zone = " - ".join(location.name.split(" - ")[:-1]) - if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "all_pokemon" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name]: continue - if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "first_stage" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name @@ -246,10 +246,10 @@ def process_pokemon_locations(self): continue if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon - or self.multiworld.catch_em_all[self.player]): + or self.options.catch_em_all): continue - if self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.area_1_to_1_mapping: place_locations = [place_location for place_location in candidate_locations if place_location.name.startswith(zone) and place_location.item.name == location.item.name] diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index de29f341c6df..fb439c4f80fa 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -194,6 +194,8 @@ def __init__(self, item_id, classification, groups): "Fuji Saved": ItemData(None, ItemClassification.progression, []), "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), "Become Champion": ItemData(None, ItemClassification.progression, []), + "Mt Moon Fossils": ItemData(None, ItemClassification.progression, []), + "Cinnabar Lab": ItemData(None, ItemClassification.progression, []), "Trainer Parties": ItemData(None, ItemClassification.filler, []) } diff --git a/worlds/pokemon_rb/level_scaling.py b/worlds/pokemon_rb/level_scaling.py index 79cda394724a..76e00d9847c4 100644 --- a/worlds/pokemon_rb/level_scaling.py +++ b/worlds/pokemon_rb/level_scaling.py @@ -10,9 +10,9 @@ def level_scaling(multiworld): while locations: sphere = set() for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if (multiworld.level_scaling[world.player] != "by_spheres_and_distance" - and (multiworld.level_scaling[world.player] != "auto" or multiworld.door_shuffle[world.player] - in ("off", "simple"))): + if (world.options.level_scaling != "by_spheres_and_distance" + and (world.options.level_scaling != "auto" + or world.options.door_shuffle in ("off", "simple"))): continue regions = {multiworld.get_region("Menu", world.player)} checked_regions = set() @@ -41,7 +41,8 @@ def reachable(): # reach them earlier. We treat them both as reachable right away for this purpose return True if (location.name == "Route 25 - Item" and state.can_reach("Route 25", "Region", location.player) - and multiworld.blind_trainers[location.player].value < 100): + and multiworld.worlds[location.player].options.blind_trainers.value < 100 + and "Route 25 - Jr. Trainer M" not in multiworld.regions.location_cache[location.player]): # Assume they will take their one chance to get the trainer to walk out of the way to reach # the item behind them return True @@ -95,9 +96,9 @@ def reachable(): if (location.item.game == "Pokemon Red and Blue" and (location.item.name.startswith("Missable ") or location.item.name.startswith("Static ")) and location.name != "Pokemon Tower 6F - Restless Soul"): - # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic static Pokemon - # are not considered for moves or evolutions, as you could release them and potentially soft lock - # the game. However, for level scaling purposes, we will treat them as not missable or static. + # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic, and static + # Pokemon are not considered for moves or evolutions, as you could release them and potentially soft + # lock the game. However, for level scaling purposes, we will treat them as not missable or static. # We would not want someone playing a minimal accessibility Dexsanity game to get what would be # technically an "out of logic" Mansion Key from selecting Bulbasaur at the beginning of the game # and end up in the Mansion early and encountering level 67 Pokémon @@ -106,7 +107,7 @@ def reachable(): else: state.collect(location.item, True, location) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if multiworld.level_scaling[world.player] == "off": + if world.options.level_scaling == "off": continue level_list_copy = level_list.copy() for sphere in spheres: @@ -136,4 +137,4 @@ def reachable(): else: sphere_objects[object].level = level_list_copy.pop(0) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - world.finished_level_scaling.set() + world.finished_level_scaling.set() \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 6aee25df2637..5885183baa9c 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -5,46 +5,48 @@ loc_id_start = 172000000 -def trainersanity(multiworld, player): - return multiworld.trainersanity[player] +def trainersanity(world, player): + include = world.trainersanity_table.pop(0) + world.trainersanity_table.append(include) + return include -def dexsanity(multiworld, player): - include = multiworld.worlds[player].dexsanity_table.pop(0) - multiworld.worlds[player].dexsanity_table.append(include) +def dexsanity(world, player): + include = world.dexsanity_table.pop(0) + world.dexsanity_table.append(include) return include -def hidden_items(multiworld, player): - return multiworld.randomize_hidden_items[player] +def hidden_items(world, player): + return world.options.randomize_hidden_items -def hidden_moon_stones(multiworld, player): - return multiworld.randomize_hidden_items[player] or multiworld.stonesanity[player] +def hidden_moon_stones(world, player): + return world.options.randomize_hidden_items or world.options.stonesanity -def tea(multiworld, player): - return multiworld.tea[player] +def tea(world, player): + return world.options.tea -def extra_key_items(multiworld, player): - return multiworld.extra_key_items[player] +def extra_key_items(world, player): + return world.options.extra_key_items -def always_on(multiworld, player): +def always_on(world, player): return True -def prizesanity(multiworld, player): - return multiworld.prizesanity[player] +def prizesanity(world, player): + return world.options.prizesanity -def split_card_key(multiworld, player): - return multiworld.split_card_key[player].value > 0 +def split_card_key(world, player): + return world.options.split_card_key.value > 0 -def not_stonesanity(multiworld, player): - return not multiworld.stonesanity[player] +def not_stonesanity(world, player): + return not world.options.stonesanity class LocationData: @@ -395,7 +397,7 @@ def __init__(self, flag): LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18), inclusion=hidden_items), LocationData("Silph Co 9F-SW", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19), inclusion=hidden_items), LocationData("Saffron Copycat's House 2F", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20), inclusion=hidden_items), - LocationData("Cerulean Cave 1F-NW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), + LocationData("Cerulean Cave 1F-SW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), LocationData("Cerulean Cave B1F-E", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24), inclusion=hidden_items), @@ -786,6 +788,8 @@ def __init__(self, flag): LocationData("Celadon Game Corner", "", "Game Corner", event=True), LocationData("Cinnabar Island", "", "Cinnabar Island", event=True), + LocationData("Cinnabar Lab", "", "Cinnabar Lab", event=True), + LocationData("Mt Moon B2F", "Mt Moon Fossils", "Mt Moon Fossils", event=True), LocationData("Celadon Department Store 4F", "Buy Poke Doll", "Buy Poke Doll", event=True), LocationData("Celadon Department Store 4F", "Buy Fire Stone", "Fire Stone", event=True, inclusion=not_stonesanity), LocationData("Celadon Department Store 4F", "Buy Water Stone", "Water Stone", event=True, inclusion=not_stonesanity), diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index cbe28e0ddb47..03e3fa3dfad0 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -1,49 +1,47 @@ from . import poke_data -def can_surf(state, player): - return (((state.has("HM03 Surf", player) and can_learn_hm(state, "Surf", player)) - or state.has("Flippers", player)) and (state.has("Soul Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Surf"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_surf(state, world, player): + return (((state.has("HM03 Surf", player) and can_learn_hm(state, world, "Surf", player))) and (state.has("Soul Badge", player) or + state.has(world.extra_badges.get("Surf"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_cut(state, player): - return ((state.has("HM01 Cut", player) and can_learn_hm(state, "Cut", player) or state.has("Master Sword", player)) - and (state.has("Cascade Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Cut"), player) or - state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_cut(state, world, player): + return ((state.has("HM01 Cut", player) and can_learn_hm(state, world, "Cut", player)) + and (state.has("Cascade Badge", player) or state.has(world.extra_badges.get("Cut"), player) or + world.options.badges_needed_for_hm_moves.value == 0)) -def can_fly(state, player): - return (((state.has("HM02 Fly", player) and can_learn_hm(state, "Fly", player)) or state.has("Flute", player)) and - (state.has("Thunder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Fly"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_fly(state, world, player): + return (((state.has("HM02 Fly", player) and can_learn_hm(state, world, "Fly", player)) or state.has("Flute", player)) and + (state.has("Thunder Badge", player) or state.has(world.extra_badges.get("Fly"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_strength(state, player): - return ((state.has("HM04 Strength", player) and can_learn_hm(state, "Strength", player)) or +def can_strength(state, world, player): + return ((state.has("HM04 Strength", player) and can_learn_hm(state, world, "Strength", player)) or state.has("Titan's Mitt", player)) and (state.has("Rainbow Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Strength"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0) + state.has(world.extra_badges.get("Strength"), player) + or world.options.badges_needed_for_hm_moves.value == 0) -def can_flash(state, player): - return (((state.has("HM05 Flash", player) and can_learn_hm(state, "Flash", player)) or state.has("Lamp", player)) - and (state.has("Boulder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Flash"), - player) or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_flash(state, world, player): + return (((state.has("HM05 Flash", player) and can_learn_hm(state, world, "Flash", player)) or state.has("Lamp", player)) + and (state.has("Boulder Badge", player) or state.has(world.extra_badges.get("Flash"), + player) or world.options.badges_needed_for_hm_moves.value == 0)) -def can_learn_hm(state, move, player): - for pokemon, data in state.multiworld.worlds[player].local_poke_data.items(): +def can_learn_hm(state, world, move, player): + for pokemon, data in world.local_poke_data.items(): if state.has(pokemon, player) and data["tms"][6] & 1 << (["Cut", "Fly", "Surf", "Strength", "Flash"].index(move) + 2): return True return False -def can_get_hidden_items(state, player): - return state.has("Item Finder", player) or not state.multiworld.require_item_finder[player].value +def can_get_hidden_items(state, world, player): + return state.has("Item Finder", player) or not world.options.require_item_finder.value def has_key_items(state, count, player): @@ -53,13 +51,14 @@ def has_key_items(state, count, player): "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", "Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone", - "Leaf Stone", "Moon Stone"] if state.has(item, player)]) + "Leaf Stone", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if state.has(item, player)]) + min(state.count("Progressive Card Key", player), 10)) return key_items >= count -def can_pass_guards(state, player): - if state.multiworld.tea[player]: +def can_pass_guards(state, world, player): + if world.options.tea: return state.has("Tea", player) else: return state.has("Vending Machine Drinks", player) @@ -70,8 +69,8 @@ def has_badges(state, count, player): "Soul Badge", "Volcano Badge", "Earth Badge"] if state.has(item, player)]) >= count -def oaks_aide(state, count, player): - return ((not state.multiworld.require_pokedex[player] or state.has("Pokedex", player)) +def oaks_aide(state, world, count, player): + return ((not world.options.require_pokedex or state.has("Pokedex", player)) and has_pokemon(state, count, player)) @@ -85,9 +84,7 @@ def has_pokemon(state, count, player): def fossil_checks(state, count, player): - return (state.can_reach('Mt Moon B2F', 'Region', player) and - state.can_reach('Cinnabar Lab Fossil Room', 'Region', player) and - state.can_reach('Cinnabar Island', 'Region', player) and len( + return (state.has_all(["Mt Moon Fossils", "Cinnabar Lab", "Cinnabar Island"], player) and len( [item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if state.has(item, player)]) >= count) @@ -96,19 +93,19 @@ def card_key(state, floor, player): state.has("Progressive Card Key", player, floor - 1) -def rock_tunnel(state, player): - return can_flash(state, player) or not state.multiworld.dark_rock_tunnel_logic[player] +def rock_tunnel(state, world, player): + return can_flash(state, world, player) or not world.options.dark_rock_tunnel_logic -def route_3(state, player): - if state.multiworld.route_3_condition[player] == "defeat_brock": +def route(state, world, player): + if world.options.route_3_condition == "defeat_brock": return state.has("Defeat Brock", player) - elif state.multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": return state.has_any(["Defeat Brock", "Defeat Misty", "Defeat Lt. Surge", "Defeat Erika", "Defeat Koga", "Defeat Blaine", "Defeat Sabrina", "Defeat Viridian Gym Giovanni"], player) - elif state.multiworld.route_3_condition[player] == "boulder_badge": + elif world.options.route_3_condition == "boulder_badge": return state.has("Boulder Badge", player) - elif state.multiworld.route_3_condition[player] == "any_badge": + elif world.options.route_3_condition == "any_badge": return state.has_any(["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"], player) # open diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 9f217e82e646..21679bec00e9 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,6 @@ -from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility +from dataclasses import dataclass +from Options import (PerGameCommonOptions, Toggle, Choice, Range, NamedRange, FreeText, TextChoice, DeathLink, + ItemsAccessibility) class GameVersion(Choice): @@ -263,12 +265,18 @@ class PrizeSanity(Toggle): default = 0 -class TrainerSanity(Toggle): - """Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating - them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and - Cinnabar Gym battles).""" +class TrainerSanity(NamedRange): + """Add location checks to trainers, which can be obtained by talking to a trainer after defeating them. Does not + affect gym leaders and some scripted event battles. You may specify a number of trainers to have checks, and in + this case they will be randomly selected. There is no in-game indication as to which trainers have checks.""" display_name = "Trainersanity" default = 0 + range_start = 0 + range_end = 317 + special_range_names = { + "disabled": 0, + "full": 317 + } class RequirePokedex(Toggle): @@ -286,19 +294,19 @@ class AllPokemonSeen(Toggle): class DexSanity(NamedRange): - """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to - have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable - Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage - of all 151 Pokemon. - If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to - Professor Oak or evaluating the Pokedex via Oak's PC.""" + """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify the exact number of Dexsanity + checks to add, and they will be distributed to Pokemon randomly. + If Accessibility is set to Full, Dexsanity checks for Pokemon that are not logically reachable will be removed, + so the number could be lower than you specified. + If Pokedex is required, the Dexsanity checks for Pokemon you acquired before acquiring the Pokedex can be found by + talking to Professor Oak or evaluating the Pokedex via Oak's PC.""" display_name = "Dexsanity" default = 0 range_start = 0 - range_end = 100 + range_end = 151 special_range_names = { "disabled": 0, - "full": 100 + "full": 151 } @@ -519,7 +527,8 @@ class TrainerLegendaries(Toggle): class BlindTrainers(Range): """Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a - battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.""" + battle. If you move into and out of their line of sight without stopping, this chance will only trigger once. + Trainers which have Trainersanity location checks ignore the Blind Trainers setting.""" display_name = "Blind Trainers" range_start = 0 range_end = 100 @@ -704,6 +713,15 @@ class RandomizeTypeChart(Choice): default = 0 +class TypeChartSeed(FreeText): + """You can enter a number to use as a seed for the type chart. If you enter anything besides a number or "random", + it will be used as a type chart group name, and everyone using the same group name will get the same type chart, + made using the type chart options of one random player within the group. If a group name is used, the type matchup + information will not be made available for trackers.""" + display_name = "Type Chart Seed" + default = "random" + + class NormalMatchups(Range): """If 'randomize' is chosen for Randomize Type Chart, this will be the weight for neutral matchups. No effect if 'chaos' is chosen""" @@ -850,8 +868,8 @@ class BicycleGateSkips(Choice): class RandomizePokemonPalettes(Choice): - """Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow - Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions), + """Modify Super Gameboy palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, + Follow Evolutions will randomize palettes but they will remain the same through evolutions (except Eeveelutions), Completely Random will randomize all Pokemons' palettes individually""" display_name = "Randomize Pokemon Palettes" option_vanilla = 0 @@ -860,104 +878,105 @@ class RandomizePokemonPalettes(Choice): option_completely_random = 3 -pokemon_rb_options = { - "accessibility": ItemsAccessibility, - "game_version": GameVersion, - "trainer_name": TrainerName, - "rival_name": RivalName, - #"goal": Goal, - "elite_four_badges_condition": EliteFourBadgesCondition, - "elite_four_key_items_condition": EliteFourKeyItemsCondition, - "elite_four_pokedex_condition": EliteFourPokedexCondition, - "victory_road_condition": VictoryRoadCondition, - "route_22_gate_condition": Route22GateCondition, - "viridian_gym_condition": ViridianGymCondition, - "cerulean_cave_badges_condition": CeruleanCaveBadgesCondition, - "cerulean_cave_key_items_condition": CeruleanCaveKeyItemsCondition, - "route_3_condition": Route3Condition, - "robbed_house_officer": RobbedHouseOfficer, - "second_fossil_check_condition": SecondFossilCheckCondition, - "fossil_check_item_types": FossilCheckItemTypes, - "exp_all": ExpAll, - "old_man": OldMan, - "badgesanity": BadgeSanity, - "badges_needed_for_hm_moves": BadgesNeededForHMMoves, - "key_items_only": KeyItemsOnly, - "tea": Tea, - "extra_key_items": ExtraKeyItems, - "split_card_key": SplitCardKey, - "all_elevators_locked": AllElevatorsLocked, - "extra_strength_boulders": ExtraStrengthBoulders, - "require_item_finder": RequireItemFinder, - "randomize_hidden_items": RandomizeHiddenItems, - "prizesanity": PrizeSanity, - "trainersanity": TrainerSanity, - "dexsanity": DexSanity, - "randomize_pokedex": RandomizePokedex, - "require_pokedex": RequirePokedex, - "all_pokemon_seen": AllPokemonSeen, - "oaks_aide_rt_2": OaksAidRt2, - "oaks_aide_rt_11": OaksAidRt11, - "oaks_aide_rt_15": OaksAidRt15, - "stonesanity": Stonesanity, - "door_shuffle": DoorShuffle, - "warp_tile_shuffle": WarpTileShuffle, - "randomize_rock_tunnel": RandomizeRockTunnel, - "dark_rock_tunnel_logic": DarkRockTunnelLogic, - "free_fly_location": FreeFlyLocation, - "town_map_fly_location": TownMapFlyLocation, - "blind_trainers": BlindTrainers, - "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, - "level_scaling": LevelScaling, - "exp_modifier": ExpModifier, - "randomize_wild_pokemon": RandomizeWildPokemon, - "area_1_to_1_mapping": Area1To1Mapping, - "randomize_starter_pokemon": RandomizeStarterPokemon, - "randomize_static_pokemon": RandomizeStaticPokemon, - "randomize_legendary_pokemon": RandomizeLegendaryPokemon, - "catch_em_all": CatchEmAll, - "randomize_pokemon_stats": RandomizePokemonStats, - "randomize_pokemon_catch_rates": RandomizePokemonCatchRates, - "minimum_catch_rate": MinimumCatchRate, - "randomize_trainer_parties": RandomizeTrainerParties, - "trainer_legendaries": TrainerLegendaries, - "move_balancing": MoveBalancing, - "fix_combat_bugs": FixCombatBugs, - "randomize_pokemon_movesets": RandomizePokemonMovesets, - "confine_transform_to_ditto": ConfineTranstormToDitto, - "start_with_four_moves": StartWithFourMoves, - "same_type_attack_bonus": SameTypeAttackBonus, - "randomize_tm_moves": RandomizeTMMoves, - "tm_same_type_compatibility": TMSameTypeCompatibility, - "tm_normal_type_compatibility": TMNormalTypeCompatibility, - "tm_other_type_compatibility": TMOtherTypeCompatibility, - "hm_same_type_compatibility": HMSameTypeCompatibility, - "hm_normal_type_compatibility": HMNormalTypeCompatibility, - "hm_other_type_compatibility": HMOtherTypeCompatibility, - "inherit_tm_hm_compatibility": InheritTMHMCompatibility, - "randomize_move_types": RandomizeMoveTypes, - "randomize_pokemon_types": RandomizePokemonTypes, - "secondary_type_chance": SecondaryTypeChance, - "randomize_type_chart": RandomizeTypeChart, - "normal_matchups": NormalMatchups, - "super_effective_matchups": SuperEffectiveMatchups, - "not_very_effective_matchups": NotVeryEffectiveMatchups, - "immunity_matchups": ImmunityMatchups, - "safari_zone_normal_battles": SafariZoneNormalBattles, - "normalize_encounter_chances": NormalizeEncounterChances, - "reusable_tms": ReusableTMs, - "better_shops": BetterShops, - "master_ball_price": MasterBallPrice, - "starting_money": StartingMoney, - "lose_money_on_blackout": LoseMoneyOnBlackout, - "poke_doll_skip": PokeDollSkip, - "bicycle_gate_skips": BicycleGateSkips, - "trap_percentage": TrapPercentage, - "poison_trap_weight": PoisonTrapWeight, - "fire_trap_weight": FireTrapWeight, - "paralyze_trap_weight": ParalyzeTrapWeight, - "sleep_trap_weight": SleepTrapWeight, - "ice_trap_weight": IceTrapWeight, - "randomize_pokemon_palettes": RandomizePokemonPalettes, - "death_link": DeathLink -} +@dataclass +class PokemonRBOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility + game_version: GameVersion + trainer_name: TrainerName + rival_name: RivalName + # goal: Goal + elite_four_badges_condition: EliteFourBadgesCondition + elite_four_key_items_condition: EliteFourKeyItemsCondition + elite_four_pokedex_condition: EliteFourPokedexCondition + victory_road_condition: VictoryRoadCondition + route_22_gate_condition: Route22GateCondition + viridian_gym_condition: ViridianGymCondition + cerulean_cave_badges_condition: CeruleanCaveBadgesCondition + cerulean_cave_key_items_condition: CeruleanCaveKeyItemsCondition + route_3_condition: Route3Condition + robbed_house_officer: RobbedHouseOfficer + second_fossil_check_condition: SecondFossilCheckCondition + fossil_check_item_types: FossilCheckItemTypes + exp_all: ExpAll + old_man: OldMan + badgesanity: BadgeSanity + badges_needed_for_hm_moves: BadgesNeededForHMMoves + key_items_only: KeyItemsOnly + tea: Tea + extra_key_items: ExtraKeyItems + split_card_key: SplitCardKey + all_elevators_locked: AllElevatorsLocked + extra_strength_boulders: ExtraStrengthBoulders + require_item_finder: RequireItemFinder + randomize_hidden_items: RandomizeHiddenItems + prizesanity: PrizeSanity + trainersanity: TrainerSanity + dexsanity: DexSanity + randomize_pokedex: RandomizePokedex + require_pokedex: RequirePokedex + all_pokemon_seen: AllPokemonSeen + oaks_aide_rt_2: OaksAidRt2 + oaks_aide_rt_11: OaksAidRt11 + oaks_aide_rt_15: OaksAidRt15 + stonesanity: Stonesanity + door_shuffle: DoorShuffle + warp_tile_shuffle: WarpTileShuffle + randomize_rock_tunnel: RandomizeRockTunnel + dark_rock_tunnel_logic: DarkRockTunnelLogic + free_fly_location: FreeFlyLocation + town_map_fly_location: TownMapFlyLocation + blind_trainers: BlindTrainers + minimum_steps_between_encounters: MinimumStepsBetweenEncounters + level_scaling: LevelScaling + exp_modifier: ExpModifier + randomize_wild_pokemon: RandomizeWildPokemon + area_1_to_1_mapping: Area1To1Mapping + randomize_starter_pokemon: RandomizeStarterPokemon + randomize_static_pokemon: RandomizeStaticPokemon + randomize_legendary_pokemon: RandomizeLegendaryPokemon + catch_em_all: CatchEmAll + randomize_pokemon_stats: RandomizePokemonStats + randomize_pokemon_catch_rates: RandomizePokemonCatchRates + minimum_catch_rate: MinimumCatchRate + randomize_trainer_parties: RandomizeTrainerParties + trainer_legendaries: TrainerLegendaries + move_balancing: MoveBalancing + fix_combat_bugs: FixCombatBugs + randomize_pokemon_movesets: RandomizePokemonMovesets + confine_transform_to_ditto: ConfineTranstormToDitto + start_with_four_moves: StartWithFourMoves + same_type_attack_bonus: SameTypeAttackBonus + randomize_tm_moves: RandomizeTMMoves + tm_same_type_compatibility: TMSameTypeCompatibility + tm_normal_type_compatibility: TMNormalTypeCompatibility + tm_other_type_compatibility: TMOtherTypeCompatibility + hm_same_type_compatibility: HMSameTypeCompatibility + hm_normal_type_compatibility: HMNormalTypeCompatibility + hm_other_type_compatibility: HMOtherTypeCompatibility + inherit_tm_hm_compatibility: InheritTMHMCompatibility + randomize_move_types: RandomizeMoveTypes + randomize_pokemon_types: RandomizePokemonTypes + secondary_type_chance: SecondaryTypeChance + randomize_type_chart: RandomizeTypeChart + normal_matchups: NormalMatchups + super_effective_matchups: SuperEffectiveMatchups + not_very_effective_matchups: NotVeryEffectiveMatchups + immunity_matchups: ImmunityMatchups + type_chart_seed: TypeChartSeed + safari_zone_normal_battles: SafariZoneNormalBattles + normalize_encounter_chances: NormalizeEncounterChances + reusable_tms: ReusableTMs + better_shops: BetterShops + master_ball_price: MasterBallPrice + starting_money: StartingMoney + lose_money_on_blackout: LoseMoneyOnBlackout + poke_doll_skip: PokeDollSkip + bicycle_gate_skips: BicycleGateSkips + trap_percentage: TrapPercentage + poison_trap_weight: PoisonTrapWeight + fire_trap_weight: FireTrapWeight + paralyze_trap_weight: ParalyzeTrapWeight + sleep_trap_weight: SleepTrapWeight + ice_trap_weight: IceTrapWeight + randomize_pokemon_palettes: RandomizePokemonPalettes + death_link: DeathLink diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 28098a2c53fe..32c0e36869da 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -3,8 +3,8 @@ from .rom_addresses import rom_addresses -def set_mon_palettes(self, random, data): - if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla": +def set_mon_palettes(world, random, data): + if world.options.randomize_pokemon_palettes == "vanilla": return pallet_map = { "Poison": 0x0F, @@ -25,9 +25,9 @@ def set_mon_palettes(self, random, data): } palettes = [] for mon in poke_data.pokemon_data: - if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type": - pallet = pallet_map[self.local_poke_data[mon]["type1"]] - elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in + if world.options.randomize_pokemon_palettes == "primary_type": + pallet = pallet_map[world.local_poke_data[mon]["type1"]] + elif (world.options.randomize_pokemon_palettes == "follow_evolutions" and mon in poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): pallet = palettes[-1] else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) @@ -93,40 +93,41 @@ def move_power(move_data): return power -def process_move_data(self): - self.local_move_data = deepcopy(poke_data.moves) +def process_move_data(world): + world.local_move_data = deepcopy(poke_data.moves) - if self.multiworld.randomize_move_types[self.player]: - for move, data in self.local_move_data.items(): + if world.options.randomize_move_types: + for move, data in world.local_move_data.items(): if move == "No Move": continue # The chance of randomized moves choosing a normal type move is high, so we want to retain having a higher # rate of normal type moves - data["type"] = self.multiworld.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) - - if self.multiworld.move_balancing[self.player]: - self.local_move_data["Sing"]["accuracy"] = 30 - self.local_move_data["Sleep Powder"]["accuracy"] = 40 - self.local_move_data["Spore"]["accuracy"] = 50 - self.local_move_data["Sonicboom"]["effect"] = 0 - self.local_move_data["Sonicboom"]["power"] = 50 - self.local_move_data["Dragon Rage"]["effect"] = 0 - self.local_move_data["Dragon Rage"]["power"] = 80 - self.local_move_data["Horn Drill"]["effect"] = 0 - self.local_move_data["Horn Drill"]["power"] = 70 - self.local_move_data["Horn Drill"]["accuracy"] = 90 - self.local_move_data["Guillotine"]["effect"] = 0 - self.local_move_data["Guillotine"]["power"] = 70 - self.local_move_data["Guillotine"]["accuracy"] = 90 - self.local_move_data["Fissure"]["effect"] = 0 - self.local_move_data["Fissure"]["power"] = 70 - self.local_move_data["Fissure"]["accuracy"] = 90 - self.local_move_data["Blizzard"]["accuracy"] = 70 - if self.multiworld.randomize_tm_moves[self.player]: - self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in - ["No Move"] + poke_data.hm_moves], 50) + data["type"] = world.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) + + if world.options.move_balancing: + world.local_move_data["Sing"]["accuracy"] = 30 + world.local_move_data["Sleep Powder"]["accuracy"] = 40 + world.local_move_data["Spore"]["accuracy"] = 50 + world.local_move_data["Sonicboom"]["effect"] = 0 + world.local_move_data["Sonicboom"]["power"] = 50 + world.local_move_data["Dragon Rage"]["effect"] = 0 + world.local_move_data["Dragon Rage"]["power"] = 80 + world.local_move_data["Horn Drill"]["effect"] = 0 + world.local_move_data["Horn Drill"]["power"] = 70 + world.local_move_data["Horn Drill"]["accuracy"] = 90 + world.local_move_data["Guillotine"]["effect"] = 0 + world.local_move_data["Guillotine"]["power"] = 70 + world.local_move_data["Guillotine"]["accuracy"] = 90 + world.local_move_data["Fissure"]["effect"] = 0 + world.local_move_data["Fissure"]["power"] = 70 + world.local_move_data["Fissure"]["accuracy"] = 90 + world.local_move_data["Blizzard"]["accuracy"] = 70 + + if world.options.randomize_tm_moves: + world.local_tms = world.random.sample([move for move in poke_data.moves.keys() if move not in + ["No Move"] + poke_data.hm_moves], 50) else: - self.local_tms = poke_data.tm_moves.copy() + world.local_tms = poke_data.tm_moves.copy() def process_pokemon_data(self): @@ -138,12 +139,12 @@ def process_pokemon_data(self): compat_hms = set() for mon, mon_data in local_poke_data.items(): - if self.multiworld.randomize_pokemon_stats[self.player] == "shuffle": + if self.options.randomize_pokemon_stats == "shuffle": stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]] if mon in poke_data.evolves_from: stat_shuffle_map = local_poke_data[poke_data.evolves_from[mon]]["stat_shuffle_map"] else: - stat_shuffle_map = self.multiworld.random.sample(range(0, 5), 5) + stat_shuffle_map = self.random.sample(range(0, 5), 5) mon_data["stat_shuffle_map"] = stat_shuffle_map mon_data["hp"] = stats[stat_shuffle_map[0]] @@ -151,7 +152,7 @@ def process_pokemon_data(self): mon_data["def"] = stats[stat_shuffle_map[2]] mon_data["spd"] = stats[stat_shuffle_map[3]] mon_data["spc"] = stats[stat_shuffle_map[4]] - elif self.multiworld.randomize_pokemon_stats[self.player] == "randomize": + elif self.options.randomize_pokemon_stats == "randomize": first_run = True while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255 or mon_data["spc"] > 255 or first_run): @@ -168,9 +169,9 @@ def process_pokemon_data(self): mon_data[stat] = 10 total_stats -= 10 assert total_stats >= 0, f"Error distributing stats for {mon} for player {self.player}" - dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100] + dist = [self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100] total_dist = sum(dist) mon_data["hp"] += int(round(dist[0] / total_dist * total_stats)) @@ -178,30 +179,30 @@ def process_pokemon_data(self): mon_data["def"] += int(round(dist[2] / total_dist * total_stats)) mon_data["spd"] += int(round(dist[3] / total_dist * total_stats)) mon_data["spc"] += int(round(dist[4] / total_dist * total_stats)) - if self.multiworld.randomize_pokemon_types[self.player]: - if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: + if self.options.randomize_pokemon_types: + if self.options.randomize_pokemon_types.value == 1 and mon in poke_data.evolves_from: type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"] if type1 == type2: - if self.multiworld.secondary_type_chance[self.player].value == -1: + if self.options.secondary_type_chance.value == -1: if mon_data["type1"] != mon_data["type2"]: while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) - elif self.multiworld.random.randint(1, 100) <= self.multiworld.secondary_type_chance[self.player].value: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) + elif self.random.randint(1, 100) <= self.options.secondary_type_chance.value: + type2 = self.random.choice(list(poke_data.type_names.values())) else: - type1 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type1 = self.random.choice(list(poke_data.type_names.values())) type2 = type1 - if ((self.multiworld.secondary_type_chance[self.player].value == -1 and mon_data["type1"] - != mon_data["type2"]) or self.multiworld.random.randint(1, 100) - <= self.multiworld.secondary_type_chance[self.player].value): + if ((self.options.secondary_type_chance.value == -1 and mon_data["type1"] + != mon_data["type2"]) or self.random.randint(1, 100) + <= self.options.secondary_type_chance.value): while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) mon_data["type1"] = type1 mon_data["type2"] = type2 - if self.multiworld.randomize_pokemon_movesets[self.player]: - if self.multiworld.randomize_pokemon_movesets[self.player] == "prefer_types": + if self.options.randomize_pokemon_movesets: + if self.options.randomize_pokemon_movesets == "prefer_types": if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal": chances = [[75, "Normal"]] elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal": @@ -219,9 +220,9 @@ def process_pokemon_data(self): moves = list(poke_data.moves.keys()) for move in ["No Move"] + poke_data.hm_moves: moves.remove(move) - if self.multiworld.confine_transform_to_ditto[self.player]: + if self.options.confine_transform_to_ditto: moves.remove("Transform") - if self.multiworld.start_with_four_moves[self.player]: + if self.options.start_with_four_moves: num_moves = 4 else: num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"], @@ -231,12 +232,12 @@ def process_pokemon_data(self): non_power_moves = [] learnsets[mon] = [] for i in range(num_moves): - if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]: + if i == 0 and mon == "Ditto" and self.options.confine_transform_to_ditto: move = "Transform" else: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) - while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + move = get_move(self.local_move_data, moves, chances, self.random) + while move == "Transform" and self.options.confine_transform_to_ditto: + move = get_move(self.local_move_data, moves, chances, self.random) if self.local_move_data[move]["power"] < 5: non_power_moves.append(move) else: @@ -244,59 +245,58 @@ def process_pokemon_data(self): learnsets[mon].sort(key=lambda move: move_power(self.local_move_data[move])) if learnsets[mon]: for move in non_power_moves: - learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move) + learnsets[mon].insert(self.random.randint(1, len(learnsets[mon])), move) else: learnsets[mon] = non_power_moves for i in range(1, 5): - if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]: + if mon_data[f"start move {i}"] != "No Move" or self.options.start_with_four_moves: mon_data[f"start move {i}"] = learnsets[mon].pop(0) - if self.multiworld.randomize_pokemon_catch_rates[self.player]: - mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player], - 255) + if self.options.randomize_pokemon_catch_rates: + mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255) else: - mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"]) + mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"]) def roll_tm_compat(roll_move): if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_same_type_compatibility[self.player].value == -1: + if self.options.hm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_same_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_same_type_compatibility[self.player].value == -1: + if self.options.tm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_same_type_compatibility.value elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_normal_type_compatibility[self.player].value == -1: + if self.options.hm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_normal_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_normal_type_compatibility[self.player].value == -1: + if self.options.tm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_normal_type_compatibility.value else: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_other_type_compatibility[self.player].value == -1: + if self.options.hm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_other_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_other_type_compatibility[self.player].value == -1: + if self.options.tm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_other_type_compatibility.value for flag, tm_move in enumerate(tms_hms): - if mon in poke_data.evolves_from and self.multiworld.inherit_tm_hm_compatibility[self.player]: + if mon in poke_data.evolves_from and self.options.inherit_tm_hm_compatibility: if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8): # always inherit learnable tms/hms @@ -310,7 +310,7 @@ def roll_tm_compat(roll_move): # so this gets full chance roll bit = roll_tm_compat(tm_move) # otherwise 50% reduced chance to add compatibility over pre-evolved form - elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): + elif self.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): bit = 1 else: bit = 0 @@ -322,15 +322,13 @@ def roll_tm_compat(roll_move): mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) hm_verify = ["Surf", "Strength"] - if self.multiworld.accessibility[self.player] != "minimal" or ((not - self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player], - self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player]) - > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")): + if self.options.accessibility != "minimal" or ((not + self.options.badgesanity) and max(self.options.elite_four_badges_condition, + self.options.route_22_gate_condition, self.options.victory_road_condition) + > 7) or (self.options.door_shuffle not in ("off", "simple")): hm_verify += ["Cut"] - if self.multiworld.accessibility[self.player] != "minimal" or (not - self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or - self.multiworld.extra_key_items[self.player]) - or self.multiworld.door_shuffle[self.player]): + if (self.options.accessibility != "minimal" or (not self.options.dark_rock_tunnel_logic) and + ((self.options.trainersanity or self.options.extra_key_items) or self.options.door_shuffle)): hm_verify += ["Flash"] # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for @@ -339,8 +337,7 @@ def roll_tm_compat(roll_move): for hm_move in hm_verify: if hm_move not in compat_hms: - mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in - poke_data.legendary_pokemon]) + mon = self.random.choice([mon for mon in poke_data.pokemon_data if mon not in poke_data.legendary_pokemon]) flag = tms_hms.index(hm_move) local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) @@ -352,7 +349,7 @@ def verify_hm_moves(multiworld, world, player): def intervene(move, test_state): move_bit = pow(2, poke_data.hm_moves.index(move) + 2) viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit] - if multiworld.randomize_wild_pokemon[player] and viable_mons: + if world.options.randomize_wild_pokemon and viable_mons: accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if loc.type == "Wild Encounter"] @@ -364,7 +361,7 @@ def number_of_zones(mon): placed_mons = [slot.item.name for slot in accessible_slots] - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: placed_mons.sort(key=lambda i: number_of_zones(i)) else: # this sort method doesn't work if you reference the same list being sorted in the lambda @@ -372,10 +369,10 @@ def number_of_zones(mon): placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) placed_mon = placed_mons.pop() - replace_mon = multiworld.random.choice(viable_mons) - replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name + replace_mon = world.random.choice(viable_mons) + replace_slot = world.random.choice([slot for slot in accessible_slots if slot.item.name == placed_mon]) - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: zone = " - ".join(replace_slot.name.split(" - ")[:-1]) replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name == placed_mon] @@ -387,7 +384,7 @@ def number_of_zones(mon): tms_hms = world.local_tms + poke_data.hm_moves flag = tms_hms.index(move) mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)] - multiworld.random.shuffle(mon_list) + world.random.shuffle(mon_list) mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]]) for mon in mon_list: @@ -399,31 +396,31 @@ def number_of_zones(mon): while True: intervene_move = None test_state = multiworld.get_all_state(False) - if not logic.can_learn_hm(test_state, "Surf", player): + if not logic.can_learn_hm(test_state, world, "Surf", player): intervene_move = "Surf" - elif not logic.can_learn_hm(test_state, "Strength", player): + elif not logic.can_learn_hm(test_state, world, "Strength", player): intervene_move = "Strength" # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, # as you will require cut to access celadon gyn - elif ((not logic.can_learn_hm(test_state, "Cut", player)) and - (multiworld.accessibility[player] != "minimal" or ((not - multiworld.badgesanity[player]) and max( - multiworld.elite_four_badges_condition[player], - multiworld.route_22_gate_condition[player], - multiworld.victory_road_condition[player]) - > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))): + elif ((not logic.can_learn_hm(test_state, world, "Cut", player)) and + (world.options.accessibility != "minimal" or ((not + world.options.badgesanity) and max( + world.options.elite_four_badges_condition, + world.options.route_22_gate_condition, + world.options.victory_road_condition) + > 7) or (world.options.door_shuffle not in ("off", "simple")))): intervene_move = "Cut" - elif ((not logic.can_learn_hm(test_state, "Flash", player)) - and multiworld.dark_rock_tunnel_logic[player] - and (multiworld.accessibility[player] != "minimal" - or multiworld.door_shuffle[player])): + elif ((not logic.can_learn_hm(test_state, world, "Flash", player)) + and world.options.dark_rock_tunnel_logic + and (world.options.accessibility != "minimal" + or world.options.door_shuffle)): intervene_move = "Flash" # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps # as reachable, and if on no door shuffle or simple, fly is simply never necessary. # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been # considered in door shuffle. - elif ((not logic.can_learn_hm(test_state, "Fly", player)) - and multiworld.door_shuffle[player] not in + elif ((not logic.can_learn_hm(test_state, world, "Fly", player)) + and world.options.door_shuffle not in ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): intervene_move = "Fly" if intervene_move: @@ -432,4 +429,4 @@ def number_of_zones(mon): intervene(intervene_move, test_state) last_intervene = intervene_move else: - break \ No newline at end of file + break diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 938c39b32090..575f4a61ca6f 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1409,21 +1409,20 @@ def pair(a, b): ['Route 2-E to Route 2 Gate', 'Route 2-SE to Route 2 Gate'], ['Cerulean City-Badge House Backyard to Cerulean Badge House', 'Cerulean City to Cerulean Badge House'], - ['Cerulean City-T to Cerulean Trashed House', - 'Cerulean City-Outskirts to Cerulean Trashed House'], - ['Fuchsia City to Fuchsia Good Rod House', - 'Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House'], - ['Route 11-E to Route 11 Gate 1F', 'Route 11-C to Route 11 Gate 1F'], - ['Route 12-N to Route 12 Gate 1F', 'Route 12-L to Route 12 Gate 1F'], - ['Route 15 to Route 15 Gate 1F', 'Route 15-W to Route 15 Gate 1F'], - ['Route 16-NE to Route 16 Gate 1F-N', 'Route 16-NW to Route 16 Gate 1F-N'], + ['Cerulean City-Outskirts to Cerulean Trashed House', + 'Cerulean City-T to Cerulean Trashed House',], + ['Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House', 'Fuchsia City to Fuchsia Good Rod House'], + ['Route 11-C to Route 11 Gate 1F', 'Route 11-E to Route 11 Gate 1F'], + ['Route 12-L to Route 12 Gate 1F', 'Route 12-N to Route 12 Gate 1F'], + ['Route 15-W to Route 15 Gate 1F', 'Route 15 to Route 15 Gate 1F'], + ['Route 16-NW to Route 16 Gate 1F-N', 'Route 16-NE to Route 16 Gate 1F-N'], ['Route 16-SW to Route 16 Gate 1F-W', 'Route 16-C to Route 16 Gate 1F-E'], ['Route 18-W to Route 18 Gate 1F-W', 'Route 18-E to Route 18 Gate 1F-E'], ['Route 5 to Route 5 Gate-N', 'Route 5-S to Route 5 Gate-S'], - ['Route 6 to Route 6 Gate-S', 'Route 6-N to Route 6 Gate-N'], + ['Route 6-N to Route 6 Gate-N', 'Route 6 to Route 6 Gate-S'], ['Route 7 to Route 7 Gate-W', 'Route 7-E to Route 7 Gate-E'], - ['Route 8 to Route 8 Gate-E', 'Route 8-W to Route 8 Gate-W'], - ['Route 22 to Route 22 Gate-S', 'Route 23-S to Route 22 Gate-N'] + ['Route 8-W to Route 8 Gate-W', 'Route 8 to Route 8 Gate-E',], + ['Route 23-S to Route 22 Gate-N', 'Route 22 to Route 22 Gate-S'] ] dungeons = [ @@ -1484,7 +1483,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, locations_per_ for location in locations_per_region.get(name, []): location.parent_region = ret ret.locations.append(location) - if multiworld.randomize_hidden_items[player] == "exclude" and "Hidden" in location.name: + if multiworld.worlds[player].options.randomize_hidden_items == "exclude" and "Hidden" in location.name: location.progress_type = LocationProgressType.EXCLUDED if exits: for exit in exits: @@ -1500,32 +1499,34 @@ def outdoor_map(name): return False -def create_regions(self): - multiworld = self.multiworld - player = self.player +def create_regions(world): + multiworld = world.multiworld + player = world.player locations_per_region = {} - start_inventory = self.multiworld.start_inventory[self.player].value.copy() - if self.multiworld.randomize_pokedex[self.player] == "start_with": + start_inventory = world.options.start_inventory.value.copy() + if world.options.randomize_pokedex == "start_with": start_inventory["Pokedex"] = 1 - self.multiworld.push_precollected(self.create_item("Pokedex")) - if self.multiworld.exp_all[self.player] == "start_with": + world.multiworld.push_precollected(world.create_item("Pokedex")) + if world.options.exp_all == "start_with": start_inventory["Exp. All"] = 1 - self.multiworld.push_precollected(self.create_item("Exp. All")) + world.multiworld.push_precollected(world.create_item("Exp. All")) + + world.item_pool = [] + combined_traps = (world.options.poison_trap_weight.value + + world.options.fire_trap_weight.value + + world.options.paralyze_trap_weight.value + + world.options.ice_trap_weight.value + + world.options.sleep_trap_weight.value) - self.item_pool = [] - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value) stones = ["Moon Stone", "Fire Stone", "Water Stone", "Thunder Stone", "Leaf Stone"] for location in location_data: locations_per_region.setdefault(location.region, []) # The check for list is so that we don't try to check the item table with a list as a key - if location.inclusion(multiworld, player) and (isinstance(location.original_item, list) or - not (self.multiworld.key_items_only[self.player] and item_table[location.original_item].classification - not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not + if location.inclusion(world, player) and (isinstance(location.original_item, list) or + not (world.options.key_items_only and item_table[location.original_item].classification + not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not location.event)): location_object = PokemonRBLocation(player, location.name, location.address, location.rom_address, location.type, location.level, location.level_address) @@ -1535,51 +1536,53 @@ def create_regions(self): event = location.event if location.original_item is None: - item = self.create_filler() - elif location.original_item == "Exp. All" and self.multiworld.exp_all[self.player] == "remove": - item = self.create_filler() + item = world.create_filler() + elif location.original_item == "Exp. All" and world.options.exp_all == "remove": + item = world.create_filler() elif location.original_item == "Pokedex": - if self.multiworld.randomize_pokedex[self.player] == "vanilla": + if world.options.randomize_pokedex == "vanilla": + location_object.event = True event = True - item = self.create_item("Pokedex") - elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]: + item = world.create_item("Pokedex") + elif location.original_item == "Moon Stone" and world.options.stonesanity: stone = stones.pop() - item = self.create_item(stone) + item = world.create_item(stone) elif location.original_item.startswith("TM"): - if self.multiworld.randomize_tm_moves[self.player]: - item = self.create_item(location.original_item.split(" ")[0]) + if world.options.randomize_tm_moves: + item = world.create_item(location.original_item.split(" ")[0]) else: - item = self.create_item(location.original_item) - elif location.original_item == "Card Key" and self.multiworld.split_card_key[self.player] == "on": - item = self.create_item("Card Key 3F") - elif "Card Key" in location.original_item and self.multiworld.split_card_key[self.player] == "progressive": - item = self.create_item("Progressive Card Key") + item = world.create_item(location.original_item) + elif location.original_item == "Card Key" and world.options.split_card_key == "on": + item = world.create_item("Card Key 3F") + elif "Card Key" in location.original_item and world.options.split_card_key == "progressive": + item = world.create_item("Progressive Card Key") else: - item = self.create_item(location.original_item) - if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) - <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): - item = self.create_item(self.select_trap()) + item = world.create_item(location.original_item) + if (item.classification == ItemClassification.filler and world.random.randint(1, 100) + <= world.options.trap_percentage.value and combined_traps != 0): + item = world.create_item(world.select_trap()) - if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All": + if (world.options.key_items_only and (location.original_item != "Exp. All") + and not (location.event or item.advancement)): continue if item.name in start_inventory and start_inventory[item.name] > 0 and \ location.original_item in item_groups["Unique"]: start_inventory[location.original_item] -= 1 - item = self.create_filler() + item = world.create_filler() if event: location_object.place_locked_item(item) if location.type == "Trainer Parties": location_object.party_data = deepcopy(location.party_data) else: - self.item_pool.append(item) + world.item_pool.append(item) - self.multiworld.random.shuffle(self.item_pool) - advancement_items = [item.name for item in self.item_pool if item.advancement] \ - + [item.name for item in self.multiworld.precollected_items[self.player] if + world.random.shuffle(world.item_pool) + advancement_items = [item.name for item in world.item_pool if item.advancement] \ + + [item.name for item in world.multiworld.precollected_items[world.player] if item.advancement] - self.total_key_items = len( + world.total_key_items = len( # The stonesanity items are not checked for here and instead just always added as the `+ 4` # They will always exist, but if stonesanity is off, then only as events. # We don't want to just add 4 if stonesanity is off while still putting them in this list in case @@ -1589,15 +1592,16 @@ def create_regions(self): "Secret Key", "Poke Flute", "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", - "Card Key 11F", "Exp. All", "Moon Stone"] if item in advancement_items]) + 4 + "Card Key 11F", "Exp. All", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if item in advancement_items]) + 4 if "Progressive Card Key" in advancement_items: - self.total_key_items += 10 + world.total_key_items += 10 - self.multiworld.cerulean_cave_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.cerulean_cave_key_items_condition[self.player].value) + world.options.cerulean_cave_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.cerulean_cave_key_items_condition.value) - self.multiworld.elite_four_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.elite_four_key_items_condition[self.player].value) + world.options.elite_four_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.elite_four_key_items_condition.value) regions = [create_region(multiworld, player, region, locations_per_region) for region in warp_data] multiworld.regions += regions @@ -1609,7 +1613,7 @@ def create_regions(self): connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Evolution", one_way=True) connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, - state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) + world.options.second_fossil_check_condition.value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") connect(multiworld, player, "Route 1", "Viridian City") connect(multiworld, player, "Viridian City", "Route 22") @@ -1617,24 +1621,24 @@ def create_regions(self): connect(multiworld, player, "Route 2-SW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 2-NW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 22 Gate-S", "Route 22 Gate-N", - lambda state: logic.has_badges(state, state.multiworld.route_22_gate_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, state.multiworld.victory_road_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, player)) + lambda state: logic.has_badges(state, world.options.route_22_gate_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, world.options.victory_road_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Viridian City-N", "Viridian City-G", lambda state: - logic.has_badges(state, state.multiworld.viridian_gym_condition[player].value, player)) - connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, player)) + logic.has_badges(state, world.options.viridian_gym_condition.value, player)) + connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 2-SW", "Viridian City-N") connect(multiworld, player, "Route 2-NW", "Pewter City") connect(multiworld, player, "Pewter City", "Pewter City-E") connect(multiworld, player, "Pewter City-M", "Pewter City", one_way=True) - connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route_3(state, player), one_way=True) + connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route(state, world, player), one_way=True) connect(multiworld, player, "Route 3", "Pewter City-E", one_way=True) connect(multiworld, player, "Route 4-W", "Route 3") - connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Mt Moon B2F", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-NE", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-C", "Mt Moon B2F-Wild", one_way=True) @@ -1644,14 +1648,14 @@ def create_regions(self): connect(multiworld, player, "Cerulean City", "Route 24") connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player)) connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True) - connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Cerulean City-Outskirts", "Route 5") - connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Route 24", "Route 25") connect(multiworld, player, "Route 9", "Route 10-N") - connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not state.multiworld.extra_key_items[player].value) + connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not world.options.extra_key_items.value) connect(multiworld, player, "Pallet Town", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Viridian City", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Route 22", "Route 22 Fishing", lambda state: state.has("Super Rod", player), one_way=True) @@ -1697,10 +1701,10 @@ def create_regions(self): connect(multiworld, player, "Pallet Town", "Old Rod Fishing", lambda state: state.has("Old Rod", player), one_way=True) connect(multiworld, player, "Pallet Town", "Good Rod Fishing", lambda state: state.has("Good Rod", player), one_way=True) connect(multiworld, player, "Cinnabar Lab Fossil Room", "Fossil Level", lambda state: logic.fossil_checks(state, 1, player), one_way=True) - connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, player)) + connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) connect(multiworld, player, "Saffron City", "Route 5-S") connect(multiworld, player, "Saffron City", "Route 6-N") connect(multiworld, player, "Saffron City", "Route 7-E") @@ -1710,59 +1714,59 @@ def create_regions(self): connect(multiworld, player, "Saffron City", "Saffron City-G", lambda state: state.has("Silph Co Liberated", player)) connect(multiworld, player, "Saffron City", "Saffron City-Silph", lambda state: state.has("Fuji Saved", player)) connect(multiworld, player, "Route 6", "Vermilion City") - connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, player) or logic.can_cut(state, player)) + connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, world, player) or logic.can_cut(state, world, player)) connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player)) connect(multiworld, player, "Vermilion City", "Route 11") - connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player)) - connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 12-L", "Lavender Town") connect(multiworld, player, "Route 10-S", "Lavender Town") connect(multiworld, player, "Route 8", "Lavender Town") - connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player])) - connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and world.options.poke_doll_skip)) + connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 7", "Celadon City") - connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Celadon City", "Route 16-E") - connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 16-E", "Route 16-C", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 17", "Route 16-SW") connect(multiworld, player, "Route 17", "Route 18-W") # connect(multiworld, player, "Pokemon Mansion 2F", "Pokemon Mansion 2F-NW", one_way=True) - connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not state.multiworld.extra_key_items[player].value, one_way=True) + connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not world.options.extra_key_items.value, one_way=True) connect(multiworld, player, "Fuchsia City", "Route 15-W") connect(multiworld, player, "Fuchsia City", "Route 18-E") connect(multiworld, player, "Route 15", "Route 14") - connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 14", "Route 13") - connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, player) or logic.can_surf(state, player) or not state.multiworld.extra_strength_boulders[player].value) + connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, world, player) or logic.can_surf(state, world, player) or not world.options.extra_strength_boulders.value) connect(multiworld, player, "Route 12-S", "Route 13-E") connect(multiworld, player, "Fuchsia City", "Route 19-N") - connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19-S") - connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 20-W", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 19-S", "Route 19/20-Water", one_way=True) - connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone West", "Safari Zone West-Wild", one_way=True) connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West-Wild", one_way=True) - connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-Wild", one_way=True) - connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, player)) - connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, player), one_way=True) + connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, world, player)) + connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, world, player), one_way=True) connect(multiworld, player, "Victory Road 3F", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F-Wild", one_way=True) @@ -1771,10 +1775,10 @@ def create_regions(self): connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-E", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-SE", "Victory Road 2F-Wild", one_way=True) - connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, player) and state.has("Victory Road Boulder", player), one_way=True) - connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, player)) + connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Victory Road Boulder", player), one_way=True) + connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, world, player)) connect(multiworld, player, "Victory Road 1F", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B1F-W", "Mt Moon B1F-Wild", one_way=True) @@ -1796,50 +1800,50 @@ def create_regions(self): connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-NE", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-SE", "Seafoam Islands B3F-Wild", one_way=True) - connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Seafoam Islands B4F-W", "Seafoam Islands B4F", one_way=True) - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, player) and logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6)) - connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or state.multiworld.old_man[player].value == 2 or logic.can_cut(state, player)) - connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, player) or not state.multiworld.extra_strength_boulders[player]) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, world, player) and logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6)) + connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or world.options.old_man.value == 2 or logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, world, player) or not world.options.extra_strength_boulders) connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-G", lambda state: state.has("Secret Key", player)) - connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not state.multiworld.extra_key_items[player].value) - connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not state.multiworld.extra_key_items[player]) or state.has("Hideout Key", player), one_way=True) + connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not world.options.extra_key_items.value) + connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not world.options.extra_key_items) or state.has("Hideout Key", player), one_way=True) connect(multiworld, player, "Celadon Game Corner-Hidden Stairs", "Celadon Game Corner", one_way=True) connect(multiworld, player, "Rocket Hideout B1F-SE", "Rocket Hideout B1F", one_way=True) - connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, state.multiworld.elite_four_badges_condition[player].value, player) and logic.has_pokemon(state, state.multiworld.elite_four_pokedex_condition[player].total, player) and logic.has_key_items(state, state.multiworld.elite_four_key_items_condition[player].total, player) and (state.has("Pokedex", player, int(state.multiworld.elite_four_pokedex_condition[player].total > 1) * state.multiworld.require_pokedex[player].value))) + connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, world.options.elite_four_badges_condition.value, player) and logic.has_pokemon(state, world.options.elite_four_pokedex_condition.total, player) and logic.has_key_items(state, world.options.elite_four_key_items_condition.total, player) and (state.has("Pokedex", player, int(world.options.elite_four_pokedex_condition.total > 1) * world.options.require_pokedex.value))) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SW", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 2F-E", "Pokemon Mansion 2F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F-SE", "Pokemon Mansion 1F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F", "Pokemon Mansion 1F-Wild", one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NW", "Cerulean Cave 1F-Wild", one_way=True) - connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-SE", one_way=True) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-NW", lambda state: logic.card_key(state, 2, player)) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-SW", lambda state: logic.card_key(state, 2, player)) @@ -1858,80 +1862,80 @@ def create_regions(self): connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player)) connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player)) connect(multiworld, player, "Silph Co 11F-W", "Silph Co 11F-C", lambda state: logic.card_key(state, 11, player)) - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B1F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B2F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B4F", lambda state: state.has("Lift Key", player)) - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Route 23-N", "Indigo Plateau") connect(multiworld, player, "Cerulean City-Water", "Cerulean City-Cave", lambda state: - logic.has_badges(state, self.multiworld.cerulean_cave_badges_condition[player].value, player) and - logic.has_key_items(state, self.multiworld.cerulean_cave_key_items_condition[player].total, player) and logic.can_surf(state, player)) + logic.has_badges(state, world.options.cerulean_cave_badges_condition.value, player) and + logic.has_key_items(state, world.options.cerulean_cave_key_items_condition.total, player) and logic.can_surf(state, world, player)) # access to any part of a city will enable flying to the Pokemon Center - connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") - connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") - connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") - connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") - connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") - connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") - connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") + connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") + connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") + connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") + connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") + connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") + connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") + connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") # drops connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)") connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)") connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)") connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) # If you haven't dropped the boulders, you'll go straight to B4F connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)") - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 2F-C", one_way=True) - if multiworld.worlds[player].fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].fly_map, - lambda state: logic.can_fly(state, player), one_way=True, name="Free Fly Location") + if world.fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.fly_map, + lambda state: logic.can_fly(state, world, player), one_way=True, name="Free Fly Location") - if multiworld.worlds[player].town_map_fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].town_map_fly_map, - lambda state: logic.can_fly(state, player) and state.has("Town Map", player), one_way=True, + if world.town_map_fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.town_map_fly_map, + lambda state: logic.can_fly(state, world, player) and state.has("Town Map", player), one_way=True, name="Town Map Fly Location") - cache = multiworld.regions.entrance_cache[self.player].copy() - if multiworld.badgesanity[player] or multiworld.door_shuffle[player] in ("off", "simple"): + cache = multiworld.regions.entrance_cache[world.player].copy() + if world.options.badgesanity or world.options.door_shuffle in ("off", "simple"): badges = None badge_locs = None else: - badges = [item for item in self.item_pool if "Badge" in item.name] + badges = [item for item in world.item_pool if "Badge" in item.name] for badge in badges: - self.item_pool.remove(badge) + world.item_pool.remove(badge) badge_locs = [multiworld.get_location(loc, player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", @@ -1939,15 +1943,18 @@ def create_regions(self): ]] for attempt in range(10): try: - door_shuffle(self, multiworld, player, badges, badge_locs) + door_shuffle(world, multiworld, player, badges, badge_locs) except DoorShuffleException as e: if attempt == 9: raise e - for region in self.multiworld.get_regions(player): + for region in world.multiworld.get_regions(player): for entrance in reversed(region.exits): if isinstance(entrance, PokemonRBWarp): region.exits.remove(entrance) - multiworld.regions.entrance_cache[self.player] = cache.copy() + for entrance in reversed(region.entrances): + if isinstance(entrance, PokemonRBWarp): + region.entrances.remove(entrance) + multiworld.regions.entrance_cache[world.player] = cache.copy() if badge_locs: for loc in badge_locs: loc.item = None @@ -1965,36 +1972,36 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): shuffle = True interior = False if not outdoor_map(region.name) and not outdoor_map(entrance_data['to']['map']): - if multiworld.door_shuffle[player] not in ("full", "insanity", "decoupled"): + if world.options.door_shuffle not in ("full", "insanity", "decoupled"): shuffle = False interior = True - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": if sorted([entrance_data['to']['map'], region.name]) == ["Celadon Game Corner-Hidden Stairs", "Rocket Hideout B1F"]: shuffle = True elif sorted([entrance_data['to']['map'], region.name]) == ["Celadon City", "Celadon Game Corner"]: shuffle = False - if (multiworld.randomize_rock_tunnel[player] and "Rock Tunnel" in region.name and "Rock Tunnel" in + if (world.options.randomize_rock_tunnel and "Rock Tunnel" in region.name and "Rock Tunnel" in entrance_data['to']['map']): shuffle = False elif (f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"]) in silph_co_warps + saffron_gym_warps: - if multiworld.warp_tile_shuffle[player]: + if world.options.warp_tile_shuffle: shuffle = True - if multiworld.warp_tile_shuffle[player] == "mixed" and multiworld.door_shuffle[player] == "full": + if world.options.warp_tile_shuffle == "mixed" and world.options.door_shuffle == "full": interior = True else: interior = False else: shuffle = False - elif not multiworld.door_shuffle[player]: + elif not world.options.door_shuffle: shuffle = False if shuffle: entrance = PokemonRBWarp(player, f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"], region, entrance_data["id"], entrance_data["address"], entrance_data["flags"] if "flags" in entrance_data else "") - if interior and multiworld.door_shuffle[player] == "full": + if interior and world.options.door_shuffle == "full": full_interiors.append(entrance) else: entrances.append(entrance) @@ -2006,22 +2013,22 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections = set() one_way_forced_connections = set() - if multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle: + if world.options.door_shuffle in ("full", "insanity", "decoupled"): safari_zone_doors = [door for pair in safari_zone_connections for door in pair] safari_zone_doors.sort() order = ["Center", "East", "North", "West"] - multiworld.random.shuffle(order) + world.random.shuffle(order) usable_doors = ["Safari Zone Gate-N to Safari Zone Center-S"] for section in order: section_doors = [door for door in safari_zone_doors if door.startswith(f"Safari Zone {section}")] - connect_door_a = multiworld.random.choice(usable_doors) - connect_door_b = multiworld.random.choice(section_doors) + connect_door_a = world.random.choice(usable_doors) + connect_door_b = world.random.choice(section_doors) usable_doors.remove(connect_door_a) section_doors.remove(connect_door_b) forced_connections.add((connect_door_a, connect_door_b)) usable_doors += section_doors - multiworld.random.shuffle(usable_doors) + world.random.shuffle(usable_doors) while usable_doors: forced_connections.add((usable_doors.pop(), usable_doors.pop())) else: @@ -2029,32 +2036,32 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): usable_safe_rooms = safe_rooms.copy() - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": forced_connections.update(simple_mandatory_connections) else: usable_safe_rooms += pokemarts - if multiworld.key_items_only[player]: + if world.options.key_items_only: usable_safe_rooms.remove("Viridian Pokemart to Viridian City") - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle in ("full", "insanity", "decoupled"): forced_connections.update(full_mandatory_connections) - r = multiworld.random.randint(0, 3) + r = world.random.randint(0, 3) if r == 2: forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + mansion_dead_ends + world.random.choice(mansion_stair_destinations + mansion_dead_ends + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] == "full": + if world.options.door_shuffle == "full": forced_connections.add(("Pokemon Mansion 1F to Pokemon Mansion 2F", "Pokemon Mansion 3F to Pokemon Mansion 2F")) elif r == 3: - dead_end = multiworld.random.randint(0, 1) + dead_end = world.random.randint(0, 1) forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", mansion_dead_ends[dead_end])) forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion B1F to Pokemon Mansion 1F-SE")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + [mansion_dead_ends[dead_end ^ 1]]))) else: forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", @@ -2062,40 +2069,40 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", mansion_dead_ends[r ^ 1])) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] in ("insanity", "decoupled"): + if world.options.door_shuffle in ("insanity", "decoupled"): usable_safe_rooms += insanity_safe_rooms - safe_rooms_sample = multiworld.random.sample(usable_safe_rooms, 6) + safe_rooms_sample = world.random.sample(usable_safe_rooms, 6) pallet_safe_room = safe_rooms_sample[-1] - for a, b in zip(multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + for a, b in zip(world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3), ["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room]): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": for a, b in zip(["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room], - multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3)): one_way_forced_connections.add((a, b)) for a, b in zip(safari_zone_houses, safe_rooms_sample): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": - for a, b in zip(multiworld.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), + if world.options.door_shuffle == "decoupled": + for a, b in zip(world.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), safari_zone_houses): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": # force Indigo Plateau Lobby to vanilla location on simple, otherwise shuffle with Pokemon Centers. - for a, b in zip(multiworld.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): + for a, b in zip(world.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): forced_connections.add((a, b)) forced_connections.add((pokemon_center_entrances[-1], pokemon_centers[-1])) - forced_pokemarts = multiworld.random.sample(pokemart_entrances, 8) - if multiworld.key_items_only[player]: + forced_pokemarts = world.random.sample(pokemart_entrances, 8) + if world.options.key_items_only: forced_pokemarts.sort(key=lambda i: i[0] != "Viridian Pokemart to Viridian City") for a, b in zip(forced_pokemarts, pokemarts): forced_connections.add((a, b)) @@ -2104,21 +2111,21 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # fly / blackout warps. Rather than mess with those coordinates (besides in Pallet Town) or have players # warping outside an entrance that isn't the Pokemon Center, just always put Pokemon Centers at Pokemon # Center entrances - for a, b in zip(multiworld.random.sample(pokemon_center_entrances, 12), pokemon_centers): + for a, b in zip(world.random.sample(pokemon_center_entrances, 12), pokemon_centers): one_way_forced_connections.add((a, b)) # Ensure a Pokemart is available at the beginning of the game - if multiworld.key_items_only[player]: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), + if world.options.key_items_only: + one_way_forced_connections.add((world.random.choice(initial_doors), "Viridian Pokemart to Viridian City")) elif "Pokemart" not in pallet_safe_room: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), multiworld.random.choice( + one_way_forced_connections.add((world.random.choice(initial_doors), world.random.choice( [mart for mart in pokemarts if mart not in safe_rooms_sample]))) - if multiworld.warp_tile_shuffle[player] == "shuffle" or (multiworld.warp_tile_shuffle[player] == "mixed" - and multiworld.door_shuffle[player] - in ("off", "simple", "interiors")): - warps = multiworld.random.sample(silph_co_warps, len(silph_co_warps)) + if world.options.warp_tile_shuffle == "shuffle" or (world.options.warp_tile_shuffle == "mixed" + and world.options.door_shuffle + in ("off", "simple", "interiors")): + warps = world.random.sample(silph_co_warps, len(silph_co_warps)) # The only warp tiles never reachable from the stairs/elevators are the two 7F-NW warps (where the rival is) # and the final 11F-W warp. As long as the two 7F-NW warps aren't connected to each other, everything should # always be reachable. @@ -2129,9 +2136,9 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # Shuffle Saffron Gym sections, then connect one warp from each section to the next. # Then connect the rest at random. - warps = multiworld.random.sample(saffron_gym_warps, len(saffron_gym_warps)) + warps = world.random.sample(saffron_gym_warps, len(saffron_gym_warps)) solution = ["SW", "W", "NW", "N", "NE", "E", "SE"] - multiworld.random.shuffle(solution) + world.random.shuffle(solution) solution = ["S"] + solution + ["C"] for i in range(len(solution) - 1): f, t = solution[i], solution[i + 1] @@ -2151,7 +2158,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add((warps.pop(), warps.pop(),)) dc_destinations = None - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations = entrances.copy() for pair in one_way_forced_connections: entrance_a = multiworld.get_entrance(pair[0], player) @@ -2179,11 +2186,11 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): full_interiors.remove(entrance_b) else: raise DoorShuffleException("Attempted to force connection with entrance not in any entrance pool, likely because it tried to force an entrance to connect twice.") - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations.remove(entrance_a) dc_destinations.remove(entrance_b) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": def connect_connecting_interiors(interior_exits, exterior_entrances): for interior, exterior in zip(interior_exits, exterior_entrances): for a, b in zip(interior, exterior): @@ -2222,68 +2229,68 @@ def connect_interiors(interior_exits, exterior_entrances): single_entrance_dungeon_entrances = dungeon_entrances.copy() for i in range(2): - if not multiworld.random.randint(0, 2): + if not world.random.randint(0, 2): placed_connecting_interior_dungeons.append(multi_purpose_dungeons[i]) interior_dungeon_entrances.append([multi_purpose_dungeon_entrances[i], None]) else: placed_single_entrance_dungeons.append(multi_purpose_dungeons[i]) single_entrance_dungeon_entrances.append(multi_purpose_dungeon_entrances[i]) - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) while placed_connecting_interior_dungeons[0] in unsafe_connecting_interior_dungeons: - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) connect_connecting_interiors(placed_connecting_interior_dungeons, interior_dungeon_entrances) interiors = connecting_interiors.copy() - multiworld.random.shuffle(interiors) + world.random.shuffle(interiors) while ((connecting_interiors[2] in (interiors[2], interiors[10], interiors[11]) # Dept Store at Dept Store # or Rt 16 Gate S or N and (interiors[11] in connecting_interiors[13:17] # Saffron Gate at Rt 16 Gate S or interiors[12] in connecting_interiors[13:17])) # Saffron Gate at Rt 18 Gate and interiors[15] in connecting_interiors[13:17] # Saffron Gate at Rt 7 Gate and interiors[1] in connecting_interiors[13:17] # Saffron Gate at Rt 7-8 Underground Path - and (not multiworld.tea[player]) and multiworld.worlds[player].fly_map != "Celadon City" - and multiworld.worlds[player].town_map_fly_map != "Celadon City"): - multiworld.random.shuffle(interiors) + and (not world.options.tea) and world.fly_map != "Celadon City" + and world.town_map_fly_map != "Celadon City"): + world.random.shuffle(interiors) connect_connecting_interiors(interiors, connecting_interior_entrances) placed_gyms = gyms.copy() - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) # Celadon Gym requires Cut access to reach the Gym Leader. There are some scenarios where its placement # could make badge placement impossible def celadon_gym_problem(): # Badgesanity or no badges needed for HM moves means gyms can go anywhere - if multiworld.badgesanity[player] or not multiworld.badges_needed_for_hm_moves[player]: + if world.options.badgesanity or not world.options.badges_needed_for_hm_moves: return False # Celadon Gym in Pewter City and need one or more badges for Viridian City gym. # No gym leaders would be reachable. - if gyms[3] == placed_gyms[0] and multiworld.viridian_gym_condition[player] > 0: + if gyms[3] == placed_gyms[0] and world.options.viridian_gym_condition > 0: return True # Celadon Gym not on Cinnabar Island or can access Viridian City gym with one badge - if not gyms[3] == placed_gyms[6] and multiworld.viridian_gym_condition[player] > 1: + if not gyms[3] == placed_gyms[6] and world.options.viridian_gym_condition > 1: return False # At this point we need to see if we can get beyond Pewter/Cinnabar with just one badge # Can get Fly access from Pewter City gym and fly beyond Pewter/Cinnabar - if multiworld.worlds[player].fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", - "Indigo Plateau") and multiworld.worlds[player].town_map_fly_map not in ("Pallet Town", + if world.fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", + "Indigo Plateau") and world.town_map_fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", "Indigo Plateau"): return False # Route 3 condition is boulder badge but Mt Moon entrance leads to safe dungeons or Rock Tunnel - if multiworld.route_3_condition[player] == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ + if world.options.route_3_condition == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ in (unsafe_connecting_interior_dungeons[0], unsafe_connecting_interior_dungeons[2]): return False # Route 3 condition is Defeat Brock and he is in Pewter City, or any other condition besides Boulder Badge. # Any badge can land in Pewter City, so the only problematic dungeon at Mt Moon is Seafoam Islands since # it requires two badges - if (((multiworld.route_3_condition[player] == "defeat_brock" and gyms[0] == placed_gyms[0]) - or multiworld.route_3_condition[player] not in ("defeat_brock", "boulder_badge")) + if (((world.options.route_3_condition == "defeat_brock" and gyms[0] == placed_gyms[0]) + or world.options.route_3_condition not in ("defeat_brock", "boulder_badge")) and placed_connecting_interior_dungeons[2] != unsafe_connecting_interior_dungeons[0]): return False @@ -2305,31 +2312,31 @@ def cerulean_city_problem(): and interiors[0] in connecting_interiors[13:17] # Saffron Gate at Underground Path North South and interiors[13] in connecting_interiors[13:17] # Saffron Gate at Route 5 Saffron Gate and multi_purpose_dungeons[0] == placed_connecting_interior_dungeons[4] # Pokémon Mansion at Rock Tunnel, which is - and (not multiworld.tea[player]) # not traversable backwards - and multiworld.route_3_condition[player] == "defeat_brock" - and multiworld.worlds[player].fly_map != "Cerulean City" - and multiworld.worlds[player].town_map_fly_map != "Cerulean City"): + and (not world.options.tea) # not traversable backwards + and world.options.route_3_condition == "defeat_brock" + and world.fly_map != "Cerulean City" + and world.town_map_fly_map != "Cerulean City"): return True while celadon_gym_problem() or cerulean_city_problem(): - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) connect_interiors(placed_gyms, gym_entrances) - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) while dungeons[4] == placed_single_entrance_dungeons[0]: # Pokémon Tower at Silph Co - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) connect_interiors(placed_single_entrance_dungeons, single_entrance_dungeon_entrances) remaining_entrances = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name)] - multiworld.random.shuffle(remaining_entrances) + world.random.shuffle(remaining_entrances) remaining_interiors = [entrance for entrance in entrances if entrance not in remaining_entrances] for entrance_a, entrance_b in zip(remaining_entrances, remaining_interiors): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] == "full": - multiworld.random.shuffle(full_interiors) + elif world.options.door_shuffle: + if world.options.door_shuffle == "full": + world.random.shuffle(full_interiors) def search_for_exit(entrance, region, checked_regions): checked_regions.add(region) @@ -2344,6 +2351,7 @@ def search_for_exit(entrance, region, checked_regions): return found_exit return None + e = multiworld.get_entrance("Underground Path Route 5 to Underground Path North South", player) while True: for entrance_a in full_interiors: if search_for_exit(entrance_a, entrance_a.parent_region, set()) is None: @@ -2363,7 +2371,7 @@ def search_for_exit(entrance, region, checked_regions): break loop_out_interiors = [] - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) for entrance in reversed(entrances): if not outdoor_map(entrance.parent_region.name): found_exit = search_for_exit(entrance, entrance.parent_region, set()) @@ -2380,26 +2388,26 @@ def search_for_exit(entrance, region, checked_regions): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player] == "interiors": + elif world.options.door_shuffle == "interiors": loop_out_interiors = [[multiworld.get_entrance(e[0], player), multiworld.get_entrance(e[1], player)] for e - in multiworld.random.sample(unsafe_connecting_interior_dungeons - + safe_connecting_interior_dungeons, 2)] + in world.random.sample(unsafe_connecting_interior_dungeons + + safe_connecting_interior_dungeons, 2)] entrances.remove(loop_out_interiors[0][1]) entrances.remove(loop_out_interiors[1][1]) - if not multiworld.badgesanity[player]: - multiworld.random.shuffle(badges) - while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]: - multiworld.random.shuffle(badges) + if not world.options.badgesanity: + world.random.shuffle(badges) + while badges[3].name == "Cascade Badge" and world.options.badges_needed_for_hm_moves: + world.random.shuffle(badges) for badge, loc in zip(badges, badge_locs): loc.place_locked_item(badge) state = multiworld.state.copy() for item, data in item_table.items(): if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \ - and ("Badge" not in item or multiworld.badgesanity[player]): + and ("Badge" not in item or world.options.badgesanity): state.collect(world.create_item(item)) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) reachable_entrances = [] relevant_events = [ @@ -2415,13 +2423,13 @@ def search_for_exit(entrance, region, checked_regions): "Victory Road Boulder", "Silph Co Liberated", ] - if multiworld.robbed_house_officer[player]: + if world.options.robbed_house_officer: relevant_events.append("Help Bill") - if multiworld.tea[player]: + if world.options.tea: relevant_events.append("Vending Machine Drinks") - if multiworld.route_3_condition[player] == "defeat_brock": + if world.options.route_3_condition == "defeat_brock": relevant_events.append("Defeat Brock") - elif multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": relevant_events += [ "Defeat Brock", "Defeat Misty", @@ -2447,7 +2455,7 @@ def adds_reachable_entrances(item): def dead_end(e): if e.can_reach(state): return True - elif multiworld.door_shuffle[player] == "decoupled": + elif world.options.door_shuffle == "decoupled": # Any unreachable exit in decoupled is not a dead end return False region = e.parent_region @@ -2482,10 +2490,10 @@ def dead_end(e): state.update_reachable_regions(player) state.sweep_for_advancements(locations=event_locations) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) - if multiworld.door_shuffle[player] == "decoupled": - multiworld.random.shuffle(dc_destinations) + if world.options.door_shuffle == "decoupled": + world.random.shuffle(dc_destinations) else: entrances.sort(key=lambda e: e.name not in entrance_only) @@ -2502,15 +2510,15 @@ def dead_end(e): is_outdoor_map = outdoor_map(entrance_a.parent_region.name) - if multiworld.door_shuffle[player] in ("interiors", "full") or len(entrances) != len(reachable_entrances): + if world.options.door_shuffle in ("interiors", "full") or len(entrances) != len(reachable_entrances): find_dead_end = False if (len(reachable_entrances) > - (1 if multiworld.door_shuffle[player] in ("insanity", "decoupled") else 8) and len(entrances) + (1 if world.options.door_shuffle in ("insanity", "decoupled") else 8) and len(entrances) <= (starting_entrances - 3)): find_dead_end = True - if (multiworld.door_shuffle[player] in ("interiors", "full") and len(entrances) < 48 + if (world.options.door_shuffle in ("interiors", "full") and len(entrances) < 48 and not is_outdoor_map): # Try to prevent a situation where the only remaining outdoor entrances are ones that cannot be # reached except by connecting directly to it. @@ -2519,9 +2527,9 @@ def dead_end(e): in reachable_entrances if not outdoor_map(entrance.parent_region.name)]) > 1: find_dead_end = True - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": destinations = dc_destinations - elif multiworld.door_shuffle[player] in ("interiors", "full"): + elif world.options.door_shuffle in ("interiors", "full"): destinations = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name) is not is_outdoor_map] if not destinations: @@ -2531,7 +2539,7 @@ def dead_end(e): destinations.sort(key=lambda e: e == entrance_a) for entrance in destinations: - if (dead_end(entrance) is find_dead_end and (multiworld.door_shuffle[player] != "decoupled" + if (dead_end(entrance) is find_dead_end and (world.options.door_shuffle != "decoupled" or entrance.parent_region.name.split("-")[0] != entrance_a.parent_region.name.split("-")[0])): entrance_b = entrance @@ -2540,28 +2548,28 @@ def dead_end(e): else: entrance_b = destinations.pop(0) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): # on Interiors/Full, the destinations variable does not point to the entrances list, so we need to # remove from that list here. entrances.remove(entrance_b) else: # Everything is reachable. Just start connecting the rest of the doors at random. - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": entrance_b = dc_destinations.pop(0) else: entrance_b = entrances.pop(0) entrance_a.connect(entrance_b) - if multiworld.door_shuffle[player] != "decoupled": + if world.options.door_shuffle != "decoupled": entrance_b.connect(entrance_a) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): for pair in loop_out_interiors: pair[1].connected_region = pair[0].connected_region pair[1].parent_region.entrances.append(pair[0]) pair[1].target = pair[0].target - if multiworld.door_shuffle[player]: + if world.options.door_shuffle: for region in multiworld.get_regions(player): checked_regions = {region} @@ -2585,10 +2593,10 @@ def check_region(region_to_check): region.entrance_hint = check_region(region) -def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, +def connect(multiworld: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None): - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) + source_region = multiworld.get_region(source, player) + target_region = multiworld.get_region(target, player) if name is None: name = source + " to " + target @@ -2604,7 +2612,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call source_region.exits.append(connection) connection.connect(target_region) if not one_way: - connect(world, player, target, source, rule, True) + connect(multiworld, player, target, source, rule, True) class PokemonRBWarp(Entrance): @@ -2621,7 +2629,7 @@ def access_rule(self, state): if self.connected_region is None: return False if "Elevator" in self.parent_region.name and ( - (state.multiworld.all_elevators_locked[self.player] + (state.multiworld.worlds[self.player].options.all_elevators_locked or "Rocket Hideout" in self.parent_region.name) and not state.has("Lift Key", self.player)): return False diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index b6c1221a29f4..5ebd204c9abc 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -13,22 +13,22 @@ from . import poke_data -def write_quizzes(self, data, random): +def write_quizzes(world, data, random): def get_quiz(q, a): if q == 0: r = random.randint(0, 3) if r == 0: - mon = self.trade_mons["Trade_Dux"] + mon = world.trade_mons["Trade_Dux"] text = "A woman inVermilion City" elif r == 1: - mon = self.trade_mons["Trade_Lola"] + mon = world.trade_mons["Trade_Lola"] text = "A man inCerulean City" elif r == 2: - mon = self.trade_mons["Trade_Marcel"] + mon = world.trade_mons["Trade_Marcel"] text = "Someone on Route 2" elif r == 3: - mon = self.trade_mons["Trade_Spot"] + mon = world.trade_mons["Trade_Spot"] text = "Someone on Route 5" if not a: answers.append(0) @@ -38,21 +38,30 @@ def get_quiz(q, a): return encode_text(f"{text}was looking for{mon}?") elif q == 1: - for location in self.multiworld.get_filled_locations(): - if location.item.name == "Secret Key" and location.item.player == self.player: + for location in world.multiworld.get_filled_locations(): + if location.item.name == "Secret Key" and location.item.player == world.player: break - player_name = self.multiworld.player_name[location.player] + player_name = world.multiworld.player_name[location.player] if not a: - if len(self.multiworld.player_name) > 1: + if len(world.multiworld.player_name) > 1: old_name = player_name while old_name == player_name: - player_name = random.choice(list(self.multiworld.player_name.values())) + player_name = random.choice(list(world.multiworld.player_name.values())) else: return encode_text("You're playingin a multiworldwith otherplayers?") - if player_name == self.multiworld.player_name[self.player]: - player_name = "yourself" - player_name = encode_text(player_name, force=True, safety=True) - return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("") + if world.multiworld.get_entrance( + "Cinnabar Island-G to Cinnabar Gym", world.player).connected_region.name == "Cinnabar Gym": + if player_name == world.multiworld.player_name[world.player]: + player_name = "yourself" + player_name = encode_text(player_name, force=True, safety=True) + return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("?") + else: + # Might not have found it yet + if player_name == world.multiworld.player_name[world.player]: + return encode_text(f"The Secret Key wasplaced inyour own world?") + player_name = encode_text(player_name, force=True, safety=True) + return (encode_text(f"The Secret Key wasplaced in") + player_name + + encode_text("'sworld?")) elif q == 2: if a: return encode_text(f"#mon ispronouncedPo-kay-mon?") @@ -62,8 +71,8 @@ def get_quiz(q, a): else: return encode_text(f"#mon ispronouncedPo-kuh-mon?") elif q == 3: - starters = [" ".join(self.multiworld.get_location( - f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + starters = [" ".join(world.multiworld.get_location( + f"Oak's Lab - Starter {i}", world.player).item.name.split(" ")[1:]) for i in range(1, 4)] mon = random.choice(starters) nots = random.choice(range(8, 16, 2)) if random.randint(0, 1): @@ -82,10 +91,10 @@ def get_quiz(q, a): return encode_text(text) elif q == 4: if a: - tm_text = self.local_tms[27] + tm_text = world.local_tms[27] else: - if self.multiworld.randomize_tm_moves[self.player]: - wrong_tms = self.local_tms.copy() + if world.options.randomize_tm_moves: + wrong_tms = world.local_tms.copy() wrong_tms.pop(27) tm_text = random.choice(wrong_tms) else: @@ -102,12 +111,36 @@ def get_quiz(q, a): i = random.randint(0, random.choice([9, 99])) return encode_text(f"POLIWAG evolves {i}times?") elif q == 7: - entity = "Motor Carrier" - if not a: - entity = random.choice(["Driver", "Shipper"]) - return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 states" - f"that the{entity}is responsiblefor planningroutes when" - "hazardousmaterials aretransported?") + q2 = random.randint(0, 2) + if q2 == 0: + entity = "Motor Carrier" + if not a: + entity = random.choice(["Driver", "Shipper"]) + return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 " + f"statesthat the{entity}is responsiblefor planning" + "routes whenhazardousmaterials aretransported?") + elif q2 == 1: + if a: + state = random.choice( + ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', + 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', + 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', + 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico', + 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', + 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', + 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']) + else: + state = "New Hampshire" + return encode_text( + f"As of 2024,{state}has a lawrequiring allfront seat vehicleoccupants to useseatbelts?") + elif q2 == 2: + if a: + country = random.choice(["The United States", "Mexico", "Canada", "Germany", "France", "China", + "Russia", "Spain", "Brazil", "Ukraine", "Saudi Arabia", "Egypt"]) + else: + country = random.choice(["The U.K.", "Pakistan", "India", "Japan", "Australia", + "New Zealand", "Thailand"]) + return encode_text(f"As of 2020,drivers in{country}drive on theright side ofthe road?") elif q == 8: mon = random.choice(list(poke_data.evolution_levels.keys())) level = poke_data.evolution_levels[mon] @@ -115,17 +148,17 @@ def get_quiz(q, a): level += random.choice(range(1, 6)) * random.choice((-1, 1)) return encode_text(f"{mon} evolvesat level {level}?") elif q == 9: - move = random.choice(list(self.local_move_data.keys())) - actual_type = self.local_move_data[move]["type"] + move = random.choice(list(world.local_move_data.keys())) + actual_type = world.local_move_data[move]["type"] question_type = actual_type while question_type == actual_type and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{move} is{question_type} type?") elif q == 10: mon = random.choice(list(poke_data.pokemon_data.keys())) - actual_type = self.local_poke_data[mon][random.choice(("type1", "type2"))] + actual_type = world.local_poke_data[mon][random.choice(("type1", "type2"))] question_type = actual_type - while question_type in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]] and not a: + while question_type in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]] and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{mon} is{question_type} type?") elif q == 11: @@ -147,8 +180,8 @@ def get_quiz(q, a): return encode_text(f"{equation}= {question_result}?") elif q == 12: route = random.choice((12, 16)) - actual_mon = self.multiworld.get_location(f"Route {route} - Sleeping Pokemon", - self.player).item.name.split("Static ")[1] + actual_mon = world.multiworld.get_location(f"Route {route} - Sleeping Pokemon", + world.player).item.name.split("Static ")[1] question_mon = actual_mon while question_mon == actual_mon and not a: question_mon = random.choice(list(poke_data.pokemon_data.keys())) @@ -157,7 +190,7 @@ def get_quiz(q, a): type1 = random.choice(list(poke_data.type_ids.keys())) type2 = random.choice(list(poke_data.type_ids.keys())) eff_msgs = ["super effective", "no ", "not veryeffective", "normal "] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[0] == type1 and matchup[1] == type2: if matchup[2] > 10: eff = eff_msgs[0] @@ -175,15 +208,25 @@ def get_quiz(q, a): eff = random.choice(eff_msgs) return encode_text(f"{type1} deals{eff}damage to{type2} type?") elif q == 14: - fossil_level = self.multiworld.get_location("Fossil Level - Trainer Parties", - self.player).party_data[0]['level'] + fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties", + world.player).party_data[0]['level'] if not a: fossil_level += random.choice((-5, 5)) return encode_text(f"Fossil #MONrevive at level{fossil_level}?") + elif q == 15: + if a: + fodmap = random.choice(["garlic", "onion", "milk", "watermelon", "cherries", "wheat", "barley", + "pistachios", "cashews", "kidney beans", "apples", "honey"]) + else: + fodmap = random.choice(["carrots", "potatoes", "oranges", "pineapple", "blueberries", "parmesan", + "eggs", "beef", "chicken", "oat", "rice", "maple syrup", "peanuts"]) + are_is = "are" if fodmap[-1] == "s" else "is" + return encode_text(f"According toMonash Uni.,{fodmap} {are_is}considered highin FODMAPs?") answers = [random.randint(0, 1) for _ in range(6)] - questions = random.sample((range(0, 15)), 6) + questions = random.sample((range(0, 16)), 6) + question_texts = [] for i, question in enumerate(questions): question_texts.append(get_quiz(question, answers[i])) @@ -193,9 +236,9 @@ def get_quiz(q, a): write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) -def generate_output(self, output_directory: str): - random = self.multiworld.per_slot_randoms[self.player] - game_version = self.multiworld.game_version[self.player].current_key +def generate_output(world, output_directory: str): + random = world.random + game_version = world.options.game_version.current_key data = bytes(get_base_rom_bytes(game_version)) base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4') @@ -205,8 +248,8 @@ def generate_output(self, output_directory: str): basemd5 = hashlib.md5() basemd5.update(data) - pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}", - self.player).connected_region.name for + pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}", + world.player).connected_region.name for entrance in ["Player's House 1F", "Oak's Lab", "Rival's House"]} paths = None @@ -222,11 +265,11 @@ def generate_output(self, output_directory: str): elif pallet_connections["Oak's Lab"] == "Player's House 1F": write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"]) - for region in self.multiworld.get_regions(self.player): + for region in world.multiworld.get_regions(world.player): for entrance in region.exits: if isinstance(entrance, PokemonRBWarp): - self.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", - self.player) + world.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", + world.player) warp_ids = (entrance.warp_id,) if isinstance(entrance.warp_id, int) else entrance.warp_id warp_to_ids = (entrance.target,) if isinstance(entrance.target, int) else entrance.target for i, warp_id in enumerate(warp_ids): @@ -241,32 +284,32 @@ def generate_output(self, output_directory: str): data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i] data[address + 1] = map_ids[connected_map_name] - if self.multiworld.door_shuffle[self.player] == "simple": + if world.options.door_shuffle == "simple": for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values(): - destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name + destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name (_, x, y, _, _, map_order_entry) = town_map_coords[destination] for map_coord_entry in map_coords_entries: data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name] - if not self.multiworld.key_items_only[self.player]: + if not world.options.key_items_only: for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM", "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM", "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")): - item_name = self.multiworld.get_location(gym_leader, self.player).item.name + item_name = world.multiworld.get_location(gym_leader, world.player).item.name if item_name.startswith("TM"): try: tm = int(item_name[2:4]) - move = poke_data.moves[self.local_tms[tm - 1]]["id"] + move = poke_data.moves[world.local_tms[tm - 1]]["id"] data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move except KeyError: pass def set_trade_mon(address, loc): - mon = self.multiworld.get_location(loc, self.player).item.name + mon = world.multiworld.get_location(loc, world.player).item.name data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] - self.trade_mons[address] = mon + world.trade_mons[address] = mon if game_version == "red": set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5") @@ -282,10 +325,10 @@ def set_trade_mon(address, loc): set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") - data[rom_addresses['Fly_Location']] = self.fly_map_code - data[rom_addresses['Map_Fly_Location']] = self.town_map_fly_map_code + data[rom_addresses['Fly_Location']] = world.fly_map_code + data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code - if self.multiworld.fix_combat_bugs[self.player]: + if world.options.fix_combat_bugs: data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1 data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de) @@ -298,25 +341,25 @@ def set_trade_mon(address, loc): data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1 - if self.multiworld.poke_doll_skip[self.player] == "in_logic": + if world.options.poke_doll_skip == "in_logic": data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop - if self.multiworld.bicycle_gate_skips[self.player] == "patched": + if world.options.bicycle_gate_skips == "patched": data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop - if self.multiworld.door_shuffle[self.player]: + if world.options.door_shuffle: data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F - if self.multiworld.all_elevators_locked[self.player]: + if world.options.all_elevators_locked: data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz - if self.multiworld.tea[self.player].value: + if world.options.tea: data[rom_addresses["Option_Tea"]] = 1 data[rom_addresses["Guard_Drink_List"]] = 0x54 data[rom_addresses["Guard_Drink_List"] + 1] = 0 @@ -325,90 +368,94 @@ def set_trade_mon(address, loc): "Oh wait there,the road's closed."), rom_addresses["Text_Saffron_Gate"]) + data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z + data[rom_addresses["Fossils_Needed_For_Second_Item"]] = ( - self.multiworld.second_fossil_check_condition[self.player].value) + world.options.second_fossil_check_condition.value) - data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value) + data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value) - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: data[rom_addresses['Option_Extra_Key_Items_A']] = 1 data[rom_addresses['Option_Extra_Key_Items_B']] = 1 data[rom_addresses['Option_Extra_Key_Items_C']] = 1 data[rom_addresses['Option_Extra_Key_Items_D']] = 1 - data[rom_addresses["Option_Split_Card_Key"]] = self.multiworld.split_card_key[self.player].value - data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55) - data[rom_addresses["Option_Cerulean_Cave_Badges"]] = self.multiworld.cerulean_cave_badges_condition[self.player].value - data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = self.multiworld.cerulean_cave_key_items_condition[self.player].total - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_badges_condition[self.player].value)), rom_addresses["Text_Cerulean_Cave_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_key_items_condition[self.player].total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) - data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.multiworld.minimum_steps_between_encounters[self.player].value - data[rom_addresses['Option_Route23_Badges']] = self.multiworld.victory_road_condition[self.player].value - data[rom_addresses['Option_Victory_Road_Badges']] = self.multiworld.route_22_gate_condition[self.player].value - data[rom_addresses['Option_Elite_Four_Pokedex']] = self.multiworld.elite_four_pokedex_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Key_Items']] = self.multiworld.elite_four_key_items_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Badges']] = self.multiworld.elite_four_badges_condition[self.player].value - write_bytes(data, encode_text(str(self.multiworld.elite_four_badges_condition[self.player].value)), rom_addresses["Text_Elite_Four_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_key_items_condition[self.player].total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_pokedex_condition[self.player].total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) - write_bytes(data, encode_text(str(self.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) - - data[rom_addresses['Option_Viridian_Gym_Badges']] = self.multiworld.viridian_gym_condition[self.player].value - data[rom_addresses['Option_EXP_Modifier']] = self.multiworld.exp_modifier[self.player].value - if not self.multiworld.require_item_finder[self.player]: + data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value + data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55) + data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value + data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total + write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"]) + write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) + data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value + data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value + data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value + data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total + data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total + data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value + write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"]) + write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) + write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) + write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) + + data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value + data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value + if not world.options.require_item_finder: data[rom_addresses['Option_Itemfinder']] = 0 # nop - if self.multiworld.extra_strength_boulders[self.player]: + if world.options.extra_strength_boulders: for i in range(0, 3): data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: for i in range(0, 4): data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 - if self.multiworld.old_man[self.player] == "open_viridian_city": + if world.options.old_man == "open_viridian_city": data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 - data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value - if self.multiworld.route_3_condition[self.player] == "open": + data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value + if world.options.route_3_condition == "open": data[rom_addresses['Option_Route3_Guard_A']] = 0x11 - if not self.multiworld.robbed_house_officer[self.player]: + if not world.options.robbed_house_officer: data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15 data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11 - if self.multiworld.require_pokedex[self.player]: + if world.options.require_pokedex: data[rom_addresses["Require_Pokedex_A"]] = 1 data[rom_addresses["Require_Pokedex_B"]] = 1 data[rom_addresses["Require_Pokedex_C"]] = 1 else: data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr - if self.multiworld.dexsanity[self.player]: + if world.options.dexsanity: data[rom_addresses["Option_Dexsanity_A"]] = 1 data[rom_addresses["Option_Dexsanity_B"]] = 1 - if self.multiworld.all_pokemon_seen[self.player]: + if world.options.all_pokemon_seen: data[rom_addresses["Option_Pokedex_Seen"]] = 1 - money = str(self.multiworld.starting_money[self.player].value).zfill(6) + money = str(world.options.starting_money.value).zfill(6) data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text( - str(self.multiworld.viridian_gym_condition[self.player].value))[0] + str(world.options.viridian_gym_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Badges_Needed"]] = encode_text( - str(self.multiworld.elite_four_badges_condition[self.player].value))[0] + str(world.options.elite_four_badges_condition.value))[0] write_bytes(data, encode_text( - " ".join(self.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", self.player).item.name.upper().split()[1:])), + " ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])), rom_addresses["Text_Magikarp_Salesman"]) - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0: + if world.options.badges_needed_for_hm_moves.value == 0: for hm_move in poke_data.hm_moves: write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), rom_addresses["HM_" + hm_move + "_Badge_a"]) - elif self.extra_badges: + elif world.extra_badges: written_badges = {} - for hm_move, badge in self.extra_badges.items(): + for hm_move, badge in world.extra_badges.items(): data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, "Soul Badge": 0x67, "Marsh Badge": 0x6F, @@ -427,7 +474,7 @@ def set_trade_mon(address, loc): write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) type_loc = rom_addresses["Type_Chart"] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10 data[type_loc] = poke_data.type_ids[matchup[0]] data[type_loc + 1] = poke_data.type_ids[matchup[1]] @@ -437,52 +484,49 @@ def set_trade_mon(address, loc): data[type_loc + 1] = 0xFF data[type_loc + 2] = 0xFF - if self.multiworld.normalize_encounter_chances[self.player].value: + if world.options.normalize_encounter_chances.value: chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] for i, chance in enumerate(chances): data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance - for mon, mon_data in self.local_poke_data.items(): + for mon, mon_data in world.local_poke_data.items(): if mon == "Mew": address = rom_addresses["Base_Stats_Mew"] else: address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) - data[address + 1] = self.local_poke_data[mon]["hp"] - data[address + 2] = self.local_poke_data[mon]["atk"] - data[address + 3] = self.local_poke_data[mon]["def"] - data[address + 4] = self.local_poke_data[mon]["spd"] - data[address + 5] = self.local_poke_data[mon]["spc"] - data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]] - data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]] - data[address + 8] = self.local_poke_data[mon]["catch rate"] - data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"] - data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"] - data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] - data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] - write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) - if mon in self.learnsets and self.learnsets[mon]: + data[address + 1] = world.local_poke_data[mon]["hp"] + data[address + 2] = world.local_poke_data[mon]["atk"] + data[address + 3] = world.local_poke_data[mon]["def"] + data[address + 4] = world.local_poke_data[mon]["spd"] + data[address + 5] = world.local_poke_data[mon]["spc"] + data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]] + data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]] + data[address + 8] = world.local_poke_data[mon]["catch rate"] + data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"] + data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"] + data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"] + data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"] + write_bytes(data, world.local_poke_data[mon]["tms"], address + 20) + if mon in world.learnsets and world.learnsets[mon]: address = rom_addresses["Learnset_" + mon.replace(" ", "")] - for i, move in enumerate(self.learnsets[mon]): + for i, move in enumerate(world.learnsets[mon]): data[(address + 1) + i * 2] = poke_data.moves[move]["id"] - data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value - data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value - data[rom_addresses["Option_Aide_Rt15"]] = self.multiworld.oaks_aide_rt_15[self.player].value + data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value + data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value + data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value - if self.multiworld.safari_zone_normal_battles[self.player].value == 1: + if world.options.safari_zone_normal_battles.value == 1: data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 - if self.multiworld.reusable_tms[self.player].value: + if world.options.reusable_tms.value: data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 - for i in range(1, 10): - data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value - - data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value) + data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value) - if self.multiworld.better_shops[self.player]: + if world.options.better_shops: inventory = ["Poke Ball", "Great Ball", "Ultra Ball"] - if self.multiworld.better_shops[self.player].value == 2: + if world.options.better_shops.value == 2: inventory.append("Master Ball") inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote", "Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", @@ -492,30 +536,30 @@ def set_trade_mon(address, loc): shop_data.append(0xFF) for shop in range(1, 11): write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"]) - if self.multiworld.stonesanity[self.player]: + if world.options.stonesanity: write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"]) - price = str(self.multiworld.master_ball_price[self.player].value).zfill(6) + price = str(world.options.master_ball_price.value).zfill(6) price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)]) write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird - for item in reversed(self.multiworld.precollected_items[self.player]): + for item in reversed(world.multiworld.precollected_items[world.player]): if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 - set_mon_palettes(self, random, data) + set_mon_palettes(world, random, data) - for move_data in self.local_move_data.values(): + for move_data in world.local_move_data.values(): if move_data["id"] == 0: continue address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) - TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms]) + TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms]) write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) - if self.multiworld.randomize_rock_tunnel[self.player]: + if world.options.randomize_rock_tunnel: seed = randomize_rock_tunnel(data, random) write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) @@ -524,44 +568,44 @@ def set_trade_mon(address, loc): data[rom_addresses['Title_Mon_First']] = mons.pop() for mon in range(0, 16): data[rom_addresses['Title_Mons'] + mon] = mons.pop() - if self.multiworld.game_version[self.player].value: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) + if world.options.game_version.value: + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) else: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) - write_bytes(data, encode_text(self.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) + write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) - slot_name = self.multiworld.player_name[self.player] + slot_name = world.multiworld.player_name[world.player] slot_name.replace("@", " ") slot_name.replace("<", " ") slot_name.replace(">", " ") write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) - if self.trainer_name == "choose_in_game": + if world.trainer_name == "choose_in_game": data[rom_addresses["Skip_Player_Name"]] = 0 else: - write_bytes(data, self.trainer_name, rom_addresses['Player_Name']) - if self.rival_name == "choose_in_game": + write_bytes(data, world.trainer_name, rom_addresses['Player_Name']) + if world.rival_name == "choose_in_game": data[rom_addresses["Skip_Rival_Name"]] = 0 else: - write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) + write_bytes(data, world.rival_name, rom_addresses['Rival_Name']) data[0xFF00] = 2 # client compatibility version - rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] rom_name.extend([0] * (21 - len(rom_name))) write_bytes(data, rom_name, 0xFFC6) - write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) - write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) + write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB) + write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0) - self.finished_level_scaling.wait() + world.finished_level_scaling.wait() - write_quizzes(self, data, random) + write_quizzes(world, data, random) - for location in self.multiworld.get_locations(self.player): + for location in world.multiworld.get_locations(world.player): if location.party_data: for party in location.party_data: if not isinstance(party["party_address"], list): @@ -588,7 +632,7 @@ def set_trade_mon(address, loc): continue elif location.rom_address is None: continue - if location.item and location.item.player == self.player: + if location.item and location.item.player == world.player: if location.rom_address: rom_address = location.rom_address if not isinstance(rom_address, list): @@ -599,7 +643,7 @@ def set_trade_mon(address, loc): elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] else: - item_id = self.item_name_to_id[location.item.name] - 172000000 + item_id = world.item_name_to_id[location.item.name] - 172000000 if item_id > 255: item_id -= 256 data[address] = item_id @@ -613,18 +657,18 @@ def set_trade_mon(address, loc): for address in rom_address: data[address] = 0x2C # AP Item - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" \ - if self.multiworld.player_name[self.player] != 'Player%d' % self.player else '' - rompath = os.path.join(output_directory, f'AP_{self.multiworld.seed_name}{outfilepname}.gb') + outfilepname = f'_P{world.player}' + outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \ + if world.multiworld.player_name[world.player] != 'Player%d' % world.player else '' + rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb') with open(rompath, 'wb') as outfile: outfile.write(data) - if self.multiworld.game_version[self.player].current_key == "red": - patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + if world.options.game_version.current_key == "red": + patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) else: - patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) patch.write() os.unlink(rompath) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index e5c073971d5d..ec233d94d44d 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,10 +1,9 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c1, "Option_Pitch_Black_Rock_Tunnel": 0x76a, - "Option_Blind_Trainers": 0x30d5, - "Option_Trainersanity1": 0x3165, - "Option_Split_Card_Key": 0x3e1e, - "Option_Fix_Combat_Bugs": 0x3e1f, + "Option_Blind_Trainers": 0x32f0, + "Option_Split_Card_Key": 0x3e19, + "Option_Fix_Combat_Bugs": 0x3e1a, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -115,9 +114,10 @@ "HM_Strength_Badge_b": 0x131ed, "HM_Flash_Badge_a": 0x131fc, "HM_Flash_Badge_b": 0x13201, - "Trainer_Screen_Total_Key_Items": 0x135dc, - "TM_Moves": 0x137b1, - "Encounter_Chances": 0x13950, + "Tea_Key_Item_A": 0x135ac, + "Trainer_Screen_Total_Key_Items": 0x1361b, + "TM_Moves": 0x137f0, + "Encounter_Chances": 0x1398f, "Warps_CeladonCity": 0x18026, "Warps_PalletTown": 0x182c7, "Warps_ViridianCity": 0x18388, @@ -128,52 +128,54 @@ "Option_Viridian_Gym_Badges": 0x1901d, "Event_Sleepy_Guy": 0x191d1, "Option_Route3_Guard_B": 0x1928a, - "Starter2_K": 0x19611, - "Starter3_K": 0x19619, - "Event_Rocket_Thief": 0x19733, - "Option_Cerulean_Cave_Badges": 0x19861, - "Option_Cerulean_Cave_Key_Items": 0x19868, - "Text_Cerulean_Cave_Badges": 0x198d7, - "Text_Cerulean_Cave_Key_Items": 0x198e5, - "Event_Stranded_Man": 0x19b3c, - "Event_Rivals_Sister": 0x19d0f, - "Warps_BluesHouse": 0x19d65, - "Warps_VermilionTradeHouse": 0x19dbc, - "Require_Pokedex_D": 0x19e53, - "Option_Elite_Four_Key_Items": 0x19e9d, - "Option_Elite_Four_Pokedex": 0x19ea4, - "Option_Elite_Four_Badges": 0x19eab, - "Text_Elite_Four_Badges": 0x19f47, - "Text_Elite_Four_Key_Items": 0x19f51, - "Text_Elite_Four_Pokedex": 0x19f64, - "Shop10": 0x1a018, - "Warps_IndigoPlateauLobby": 0x1a044, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a16c, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a17a, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a188, - "Event_SKC4F": 0x1a19b, - "Warps_SilphCo4F": 0x1a21d, - "Missable_Silph_Co_4F_Item_1": 0x1a25d, - "Missable_Silph_Co_4F_Item_2": 0x1a264, - "Missable_Silph_Co_4F_Item_3": 0x1a26b, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3c3, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3d1, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3df, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3ed, - "Event_SKC5F": 0x1a400, - "Warps_SilphCo5F": 0x1a4aa, - "Missable_Silph_Co_5F_Item_1": 0x1a4f2, - "Missable_Silph_Co_5F_Item_2": 0x1a4f9, - "Missable_Silph_Co_5F_Item_3": 0x1a500, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a630, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a63e, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a64c, - "Event_SKC6F": 0x1a66d, - "Warps_SilphCo6F": 0x1a74b, - "Missable_Silph_Co_6F_Item_1": 0x1a79b, - "Missable_Silph_Co_6F_Item_2": 0x1a7a2, - "Path_Pallet_Oak": 0x1a928, - "Path_Pallet_Player": 0x1a935, + "Starter2_K": 0x19618, + "Starter3_K": 0x19620, + "Event_Rocket_Thief": 0x1973a, + "Tea_Key_Item_C": 0x1988f, + "Option_Cerulean_Cave_Badges": 0x198a0, + "Option_Cerulean_Cave_Key_Items": 0x198a7, + "Text_Cerulean_Cave_Badges": 0x19916, + "Text_Cerulean_Cave_Key_Items": 0x19924, + "Event_Stranded_Man": 0x19b7b, + "Event_Rivals_Sister": 0x19d4e, + "Warps_BluesHouse": 0x19da4, + "Warps_VermilionTradeHouse": 0x19dfb, + "Require_Pokedex_D": 0x19e99, + "Tea_Key_Item_B": 0x19f13, + "Option_Elite_Four_Key_Items": 0x19f1b, + "Option_Elite_Four_Pokedex": 0x19f22, + "Option_Elite_Four_Badges": 0x19f29, + "Text_Elite_Four_Badges": 0x19fc5, + "Text_Elite_Four_Key_Items": 0x19fcf, + "Text_Elite_Four_Pokedex": 0x19fe2, + "Shop10": 0x1a096, + "Warps_IndigoPlateauLobby": 0x1a0c2, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a1ea, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a1f8, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a206, + "Event_SKC4F": 0x1a219, + "Warps_SilphCo4F": 0x1a29b, + "Missable_Silph_Co_4F_Item_1": 0x1a2db, + "Missable_Silph_Co_4F_Item_2": 0x1a2e2, + "Missable_Silph_Co_4F_Item_3": 0x1a2e9, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a441, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a44f, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a45d, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a46b, + "Event_SKC5F": 0x1a47e, + "Warps_SilphCo5F": 0x1a528, + "Missable_Silph_Co_5F_Item_1": 0x1a570, + "Missable_Silph_Co_5F_Item_2": 0x1a577, + "Missable_Silph_Co_5F_Item_3": 0x1a57e, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a6ae, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a6bc, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a6ca, + "Event_SKC6F": 0x1a6eb, + "Warps_SilphCo6F": 0x1a7c9, + "Missable_Silph_Co_6F_Item_1": 0x1a819, + "Missable_Silph_Co_6F_Item_2": 0x1a820, + "Path_Pallet_Oak": 0x1a9a6, + "Path_Pallet_Player": 0x1a9b3, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, @@ -191,75 +193,75 @@ "Starter2_E": 0x1d2f7, "Starter3_E": 0x1d2ff, "Event_Pokedex": 0x1d363, - "Event_Oaks_Gift": 0x1d393, - "Starter2_P": 0x1d481, - "Starter3_P": 0x1d489, - "Warps_OaksLab": 0x1d6af, - "Event_Pokemart_Quest": 0x1d76b, - "Shop1": 0x1d795, - "Warps_ViridianMart": 0x1d7d8, - "Warps_ViridianSchoolHouse": 0x1d82b, - "Warps_ViridianNicknameHouse": 0x1d889, - "Warps_PewterNidoranHouse": 0x1d8e4, - "Warps_PewterSpeechHouse": 0x1d927, - "Warps_CeruleanTrashedHouse": 0x1d98d, - "Warps_CeruleanTradeHouse": 0x1d9de, - "Event_Bicycle_Shop": 0x1da2f, - "Bike_Shop_Item_Display": 0x1da8a, - "Warps_BikeShop": 0x1db45, - "Event_Fuji": 0x1dbfd, - "Warps_MrFujisHouse": 0x1dc44, - "Warps_LavenderCuboneHouse": 0x1dcc0, - "Warps_NameRatersHouse": 0x1ddae, - "Warps_VermilionPidgeyHouse": 0x1ddf8, - "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de4e, - "Warps_VermilionDock": 0x1de70, - "Static_Encounter_Mew": 0x1de7e, - "Gift_Eevee": 0x1def7, - "Warps_CeladonMansionRoofHouse": 0x1df0e, - "Shop7": 0x1df49, - "Warps_FuchsiaMart": 0x1df74, - "Warps_SaffronPidgeyHouse": 0x1dfdd, - "Event_Mr_Psychic": 0x1e020, - "Warps_MrPsychicsHouse": 0x1e05d, - "Warps_DiglettsCaveRoute2": 0x1e092, - "Warps_Route2TradeHouse": 0x1e0da, - "Warps_Route5Gate": 0x1e1db, - "Warps_Route6Gate": 0x1e2ad, - "Warps_Route7Gate": 0x1e383, - "Warps_Route8Gate": 0x1e454, - "Warps_UndergroundPathRoute8": 0x1e4a5, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e511, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e51f, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e52d, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e53b, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e549, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e557, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e565, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e573, - "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e581, - "Warps_PowerPlant": 0x1e5de, - "Static_Encounter_Voltorb_A": 0x1e5f0, - "Static_Encounter_Voltorb_B": 0x1e5f8, - "Static_Encounter_Voltorb_C": 0x1e600, - "Static_Encounter_Electrode_A": 0x1e608, - "Static_Encounter_Voltorb_D": 0x1e610, - "Static_Encounter_Voltorb_E": 0x1e618, - "Static_Encounter_Electrode_B": 0x1e620, - "Static_Encounter_Voltorb_F": 0x1e628, - "Static_Encounter_Zapdos": 0x1e630, - "Missable_Power_Plant_Item_1": 0x1e638, - "Missable_Power_Plant_Item_2": 0x1e63f, - "Missable_Power_Plant_Item_3": 0x1e646, - "Missable_Power_Plant_Item_4": 0x1e64d, - "Missable_Power_Plant_Item_5": 0x1e654, - "Warps_DiglettsCaveRoute11": 0x1e7e9, - "Event_Rt16_House_Woman": 0x1e827, - "Warps_Route16FlyHouse": 0x1e870, - "Option_Victory_Road_Badges": 0x1e8f3, - "Warps_Route22Gate": 0x1e9e3, - "Event_Bill": 0x1eb24, - "Warps_BillsHouse": 0x1eb83, + "Event_Oaks_Gift": 0x1d398, + "Starter2_P": 0x1d486, + "Starter3_P": 0x1d48e, + "Warps_OaksLab": 0x1d6b4, + "Event_Pokemart_Quest": 0x1d770, + "Shop1": 0x1d79a, + "Warps_ViridianMart": 0x1d7dd, + "Warps_ViridianSchoolHouse": 0x1d830, + "Warps_ViridianNicknameHouse": 0x1d88e, + "Warps_PewterNidoranHouse": 0x1d8e9, + "Warps_PewterSpeechHouse": 0x1d92c, + "Warps_CeruleanTrashedHouse": 0x1d992, + "Warps_CeruleanTradeHouse": 0x1d9e3, + "Event_Bicycle_Shop": 0x1da34, + "Bike_Shop_Item_Display": 0x1da8f, + "Warps_BikeShop": 0x1db4a, + "Event_Fuji": 0x1dc02, + "Warps_MrFujisHouse": 0x1dc49, + "Warps_LavenderCuboneHouse": 0x1dcc5, + "Warps_NameRatersHouse": 0x1ddb3, + "Warps_VermilionPidgeyHouse": 0x1ddfd, + "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de53, + "Warps_VermilionDock": 0x1de75, + "Static_Encounter_Mew": 0x1de83, + "Gift_Eevee": 0x1defc, + "Warps_CeladonMansionRoofHouse": 0x1df13, + "Shop7": 0x1df4e, + "Warps_FuchsiaMart": 0x1df79, + "Warps_SaffronPidgeyHouse": 0x1dfe2, + "Event_Mr_Psychic": 0x1e025, + "Warps_MrPsychicsHouse": 0x1e062, + "Warps_DiglettsCaveRoute2": 0x1e097, + "Warps_Route2TradeHouse": 0x1e0df, + "Warps_Route5Gate": 0x1e1e0, + "Warps_Route6Gate": 0x1e2b2, + "Warps_Route7Gate": 0x1e388, + "Warps_Route8Gate": 0x1e459, + "Warps_UndergroundPathRoute8": 0x1e4aa, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e516, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e524, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e532, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e540, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e54e, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e55c, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e56a, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e578, + "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e586, + "Warps_PowerPlant": 0x1e5e3, + "Static_Encounter_Voltorb_A": 0x1e5f5, + "Static_Encounter_Voltorb_B": 0x1e5fd, + "Static_Encounter_Voltorb_C": 0x1e605, + "Static_Encounter_Electrode_A": 0x1e60d, + "Static_Encounter_Voltorb_D": 0x1e615, + "Static_Encounter_Voltorb_E": 0x1e61d, + "Static_Encounter_Electrode_B": 0x1e625, + "Static_Encounter_Voltorb_F": 0x1e62d, + "Static_Encounter_Zapdos": 0x1e635, + "Missable_Power_Plant_Item_1": 0x1e63d, + "Missable_Power_Plant_Item_2": 0x1e644, + "Missable_Power_Plant_Item_3": 0x1e64b, + "Missable_Power_Plant_Item_4": 0x1e652, + "Missable_Power_Plant_Item_5": 0x1e659, + "Warps_DiglettsCaveRoute11": 0x1e7ee, + "Event_Rt16_House_Woman": 0x1e82c, + "Warps_Route16FlyHouse": 0x1e875, + "Option_Victory_Road_Badges": 0x1e8f8, + "Warps_Route22Gate": 0x1e9e8, + "Event_Bill": 0x1eb29, + "Warps_BillsHouse": 0x1eb88, "Starter1_O": 0x372b0, "Starter2_O": 0x372b4, "Starter3_O": 0x372b8, @@ -1470,74 +1472,73 @@ "Trainersanity_EVENT_BEAT_POKEMONTOWER_5_TRAINER_3_ITEM": 0x609ea, "Warps_PokemonTower5F": 0x60a5e, "Missable_Pokemon_Tower_5F_Item": 0x60a92, - "Option_Trainersanity2": 0x60b2a, - "Ghost_Battle1": 0x60b83, - "Ghost_Battle_Level": 0x60b88, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c25, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c33, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c41, - "Ghost_Battle2": 0x60c69, - "Warps_PokemonTower6F": 0x60cbe, - "Missable_Pokemon_Tower_6F_Item_1": 0x60ce4, - "Missable_Pokemon_Tower_6F_Item_2": 0x60ceb, - "Entrance_Shuffle_Fuji_Warp": 0x60deb, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60edf, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eed, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60efb, - "Warps_PokemonTower7F": 0x60f8b, - "Warps_CeladonMart1F": 0x61033, - "Gift_Aerodactyl": 0x610f5, - "Gift_Omanyte": 0x610f9, - "Gift_Kabuto": 0x610fd, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611de, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611ec, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611fa, - "Warps_ViridianForest": 0x61273, - "Missable_Viridian_Forest_Item_1": 0x612c1, - "Missable_Viridian_Forest_Item_2": 0x612c8, - "Missable_Viridian_Forest_Item_3": 0x612cf, - "Warps_SSAnne1F": 0x61310, - "Starter2_M": 0x614e5, - "Starter3_M": 0x614ed, - "Warps_SSAnne2F": 0x615ab, - "Warps_SSAnneB1F": 0x616c9, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61771, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6177f, - "Warps_SSAnneBow": 0x617c6, - "Warps_SSAnneKitchen": 0x618b6, - "Event_SS_Anne_Captain": 0x6194e, - "Warps_SSAnneCaptainsRoom": 0x619d5, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a3d, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a4b, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a59, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a67, - "Warps_SSAnne1FRooms": 0x61af7, - "Missable_SS_Anne_1F_Item": 0x61b53, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c24, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c32, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c40, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c4e, - "Warps_SSAnne2FRooms": 0x61d2c, - "Missable_SS_Anne_2F_Item_1": 0x61d88, - "Missable_SS_Anne_2F_Item_2": 0x61d9b, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e2c, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e3a, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e48, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e56, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e64, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e72, - "Warps_SSAnneB1FRooms": 0x61f20, - "Missable_SS_Anne_B1F_Item_1": 0x61f8a, - "Missable_SS_Anne_B1F_Item_2": 0x61f91, - "Missable_SS_Anne_B1F_Item_3": 0x61f98, - "Warps_UndergroundPathNorthSouth": 0x61fd5, - "Warps_UndergroundPathWestEast": 0x61ff9, - "Warps_DiglettsCave": 0x6201d, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62358, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62366, - "Event_Silph_Co_President": 0x62373, - "Event_SKC11F": 0x623bd, - "Warps_SilphCo11F": 0x62446, + "Ghost_Battle1": 0x60b93, + "Ghost_Battle_Level": 0x60b98, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c35, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c43, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c51, + "Ghost_Battle2": 0x60c79, + "Warps_PokemonTower6F": 0x60cce, + "Missable_Pokemon_Tower_6F_Item_1": 0x60cf4, + "Missable_Pokemon_Tower_6F_Item_2": 0x60cfb, + "Entrance_Shuffle_Fuji_Warp": 0x60dfb, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eef, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60efd, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60f0b, + "Warps_PokemonTower7F": 0x60f9b, + "Warps_CeladonMart1F": 0x61043, + "Gift_Aerodactyl": 0x61105, + "Gift_Omanyte": 0x61109, + "Gift_Kabuto": 0x6110d, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x61209, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x61217, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x61225, + "Warps_ViridianForest": 0x6129e, + "Missable_Viridian_Forest_Item_1": 0x612ec, + "Missable_Viridian_Forest_Item_2": 0x612f3, + "Missable_Viridian_Forest_Item_3": 0x612fa, + "Warps_SSAnne1F": 0x6133b, + "Starter2_M": 0x61510, + "Starter3_M": 0x61518, + "Warps_SSAnne2F": 0x615d6, + "Warps_SSAnneB1F": 0x616f4, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6179c, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x617aa, + "Warps_SSAnneBow": 0x617f1, + "Warps_SSAnneKitchen": 0x618e1, + "Event_SS_Anne_Captain": 0x61979, + "Warps_SSAnneCaptainsRoom": 0x61a00, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a68, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a76, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a84, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a92, + "Warps_SSAnne1FRooms": 0x61b22, + "Missable_SS_Anne_1F_Item": 0x61b7e, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c4f, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c5d, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c6b, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c79, + "Warps_SSAnne2FRooms": 0x61d57, + "Missable_SS_Anne_2F_Item_1": 0x61db3, + "Missable_SS_Anne_2F_Item_2": 0x61dc6, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e57, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e65, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e73, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e81, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e8f, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e9d, + "Warps_SSAnneB1FRooms": 0x61f4b, + "Missable_SS_Anne_B1F_Item_1": 0x61fb5, + "Missable_SS_Anne_B1F_Item_2": 0x61fbc, + "Missable_SS_Anne_B1F_Item_3": 0x61fc3, + "Warps_UndergroundPathNorthSouth": 0x62000, + "Warps_UndergroundPathWestEast": 0x62024, + "Warps_DiglettsCave": 0x62048, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62383, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62391, + "Event_Silph_Co_President": 0x6239e, + "Event_SKC11F": 0x623e8, + "Warps_SilphCo11F": 0x62471, "Ghost_Battle4": 0x708e1, "Town_Map_Order": 0x70f0f, "Town_Map_Coords": 0x71381, @@ -1589,44 +1590,37 @@ "Warps_FuchsiaMeetingRoom": 0x75879, "Badge_Cinnabar_Gym": 0x759de, "Event_Cinnabar_Gym": 0x759f2, - "Option_Trainersanity4": 0x75ace, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75ada, - "Option_Trainersanity3": 0x75b1e, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2a, - "Option_Trainersanity5": 0x75b85, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b91, - "Option_Trainersanity6": 0x75bd5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be1, - "Option_Trainersanity7": 0x75c25, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c31, - "Option_Trainersanity8": 0x75c75, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c81, - "Option_Trainersanity9": 0x75cc5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cd1, - "Warps_CinnabarGym": 0x75d1b, - "Warps_CinnabarLab": 0x75e02, - "Warps_CinnabarLabTradeRoom": 0x75e94, - "Event_Lab_Scientist": 0x75ee9, - "Warps_CinnabarLabMetronomeRoom": 0x75f35, - "Fossils_Needed_For_Second_Item": 0x75fb6, - "Fossil_Level": 0x76017, - "Event_Dome_Fossil_B": 0x76031, - "Event_Helix_Fossil_B": 0x76051, - "Warps_CinnabarLabFossilRoom": 0x760d2, - "Warps_CinnabarPokecenter": 0x76128, - "Shop8": 0x7616f, - "Warps_CinnabarMart": 0x7619b, - "Warps_CopycatsHouse1F": 0x761ed, - "Starter2_N": 0x762a2, - "Starter3_N": 0x762aa, - "Warps_ChampionsRoom": 0x764d5, - "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76604, - "Warps_LoreleisRoom": 0x76628, - "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7675d, - "Warps_BrunosRoom": 0x76781, - "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768bc, - "Warps_AgathasRoom": 0x768e0, - "Option_Itemfinder": 0x76a33, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75adc, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2e, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b97, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be9, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c3b, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c8d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cdf, + "Warps_CinnabarGym": 0x75d29, + "Warps_CinnabarLab": 0x75e10, + "Warps_CinnabarLabTradeRoom": 0x75ea2, + "Event_Lab_Scientist": 0x75ef7, + "Warps_CinnabarLabMetronomeRoom": 0x75f43, + "Fossils_Needed_For_Second_Item": 0x75fc4, + "Fossil_Level": 0x76025, + "Event_Dome_Fossil_B": 0x7603f, + "Event_Helix_Fossil_B": 0x7605f, + "Warps_CinnabarLabFossilRoom": 0x760e0, + "Warps_CinnabarPokecenter": 0x76136, + "Shop8": 0x7617d, + "Warps_CinnabarMart": 0x761a9, + "Warps_CopycatsHouse1F": 0x761fb, + "Starter2_N": 0x762b0, + "Starter3_N": 0x762b8, + "Warps_ChampionsRoom": 0x764e3, + "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76612, + "Warps_LoreleisRoom": 0x76636, + "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7676b, + "Warps_BrunosRoom": 0x7678f, + "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768ca, + "Warps_AgathasRoom": 0x768ee, + "Option_Itemfinder": 0x76a41, "Text_Quiz_A": 0x88806, "Text_Quiz_B": 0x8893a, "Text_Quiz_C": 0x88a6e, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 1d68f3148963..ba4bfd471c52 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -3,7 +3,7 @@ from . import logic -def set_rules(multiworld, player): +def set_rules(multiworld, world, player): item_rules = { # Some items do special things when they are passed into the GiveItem function in the game, but @@ -15,54 +15,46 @@ def set_rules(multiworld, player): not in i.name) } - if multiworld.prizesanity[player]: + if world.options.prizesanity: def prize_rule(i): return i.player != player or i.name in item_groups["Unique"] item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule - if multiworld.accessibility[player] != "full": - multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item: - item.name == "Bike Voucher" - and item.player == player) - multiworld.get_location("Fuchsia Warden's House - Safari Zone Warden", player).always_allow = (lambda state, item: - item.name == "Gold Teeth" and - item.player == player) - access_rules = { "Rival's House - Rival's Sister": lambda state: state.has("Oak's Parcel", player), "Oak's Lab - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), - "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, player) or logic.can_surf(state, player), - "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_2[player].value + 5, player), + "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, world, player) or logic.can_surf(state, world, player), + "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_2.value + 5, player), "Cerulean Bicycle Shop": lambda state: state.has("Bike Voucher", player) or location_item_name(state, "Cerulean Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Mr. Fuji's House - Mr. Fuji": lambda state: state.has("Fuji Saved", player), - "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_11[player].value + 5, player), - "Celadon City - Stranded Man": lambda state: logic.can_surf(state, player), + "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_11.value + 5, player), + "Celadon City - Stranded Man": lambda state: logic.can_surf(state, world, player), "Fuchsia Warden's House - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) or location_item_name(state, "Fuchsia Warden's House - Safari Zone Warden", player) == ("Gold Teeth", player), - "Route 12 - Island Item": lambda state: logic.can_surf(state, player), - "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_15[player].value + 5, player), - "Route 25 - Item": lambda state: logic.can_cut(state, player), - "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, player), - "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, player), + "Route 12 - Island Item": lambda state: logic.can_surf(state, world, player), + "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_15.value + 5, player), + "Route 25 - Item": lambda state: logic.can_cut(state, world, player), + "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, world, player), + "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, world, player), "Saffron Copycat's House 2F - Copycat": lambda state: state.has("Buy Poke Doll", player), "Celadon Game Corner - West Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - Center Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - East Gambler's Gift": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), + "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), "Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), "Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), @@ -79,9 +71,9 @@ def prize_rule(i): "Cinnabar Lab Fossil Room - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player) and state.has("Cinnabar Island", player), "Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), "Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), - "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, player) and state.has("Seafoam Boss Boulders", player), - "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, player), - "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, player), + "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Boss Boulders", player), + "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), + "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), **{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)}, "Pokemon Tower 6F - Restless Soul": lambda state: state.has("Silph Scope", player), # just for level scaling @@ -103,101 +95,101 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), # # Rock Tunnel - "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, world, player), # Pokédex check "Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), # Hidden items - "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, world, player), - "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, player), + "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, world, player), "Route 10 - Hidden Item Behind Rock Tunnel Entrance Cuttable Tree": lambda - state: logic.can_get_hidden_items(state, player) and logic.can_cut(state, player), - "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player) and logic.can_cut(state, world, player), + "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, world, player), "Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: - logic.can_get_hidden_items(state, player), - "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, + logic.can_get_hidden_items(state, world, player), + "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, player), - "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, + "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, world, player), - "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, + "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, world, player), - "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, player), - "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), + "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, world, player), + "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), # if you can reach any exit boulders, that means you can drop into the water tunnel and auto-surf - "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, player), + "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, world, player), "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda - state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, player), - "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, world, player), - "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, player), - "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, player), - "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, player), + "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, world, player), + "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Northern Stairs": lambda - state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Southern Stairs": lambda - state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, player), - "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, world, player), + "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, player), - "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, player), + "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, world, player), # Evolutions "Evolution - Ivysaur": lambda state: state.has("Bulbasaur", player) and logic.evolve_level(state, 16, player), @@ -281,5 +273,4 @@ def prize_rule(i): if loc.name.startswith("Pokedex"): mon = loc.name.split(" - ")[1] add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not - state.multiworld.require_pokedex[player]) and (state.has(i, player) - or state.has(f"Static {i}", player))) + world.options.require_pokedex) and (state.has(i, player) or state.has(f"Static {i}", player))) From 0d35cd4679f6f267bfbdb1b91325eb1cb2c6ee39 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 18 Sep 2024 11:42:22 -0700 Subject: [PATCH 284/393] BizHawkClient: Avoid error launching BizHawkClient via Launcher CLI (#3554) * Core, BizHawkClient: Support launching BizHawkClient via Launcher command line * Revert changes to LauncherComponents.py --- BizHawkClient.py | 3 ++- worlds/_bizhawk/client.py | 2 +- worlds/_bizhawk/context.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/BizHawkClient.py b/BizHawkClient.py index 86c8e5197e3f..743785b25f16 100644 --- a/BizHawkClient.py +++ b/BizHawkClient.py @@ -1,9 +1,10 @@ from __future__ import annotations +import sys import ModuleUpdate ModuleUpdate.update() from worlds._bizhawk.context import launch if __name__ == "__main__": - launch() + launch(*sys.argv[1:]) diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 00370c277a17..415b663e60af 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -15,7 +15,7 @@ def launch_client(*args) -> None: from .context import launch - launch_subprocess(launch, name="BizHawkClient") + launch_subprocess(launch, name="BizHawkClient", args=args) component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 896c8fb7b504..2a3965a54fcd 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -239,11 +239,11 @@ async def _patch_and_run_game(patch_file: str): logger.exception(exc) -def launch() -> None: +def launch(*launch_args) -> None: async def main(): parser = get_base_parser() parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") - args = parser.parse_args() + args = parser.parse_args(launch_args) ctx = BizHawkClientContext(args.connect, args.password) ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") From 2ee8b7535dcf0ca22b5dfb84bbc01570fd5ed69f Mon Sep 17 00:00:00 2001 From: Faris <162540354+FarisTheAncient@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:53:17 -0500 Subject: [PATCH 285/393] OSRS: UT integration for OSRS to support chunksanity (#3776) --- worlds/osrs/__init__.py | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 49aa1666084e..9ed55f218d9f 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -90,16 +90,18 @@ def generate_early(self) -> None: rnd = self.random starting_area = self.options.starting_area + + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT + if not hasattr(self.multiworld, "generation_is_fake"): + if starting_area.value == StartingArea.option_any_bank: + self.starting_area_item = rnd.choice(starting_area_dict) + elif starting_area.value < StartingArea.option_chunksanity: + self.starting_area_item = starting_area_dict[starting_area.value] + else: + self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - if starting_area.value == StartingArea.option_any_bank: - self.starting_area_item = rnd.choice(starting_area_dict) - elif starting_area.value < StartingArea.option_chunksanity: - self.starting_area_item = starting_area_dict[starting_area.value] - else: - self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - - # Set Starting Chunk - self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + # Set Starting Chunk + self.multiworld.push_precollected(self.create_item(self.starting_area_item)) """ This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. @@ -109,8 +111,23 @@ def generate_early(self) -> None: def fill_slot_data(self): data = self.options.as_dict("brutal_grinds") data["data_csv_tag"] = data_csv_tag + data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv return data + def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: + if "starting_area" in slot_data: + self.starting_area_item = slot_data["starting_area"] + menu_region = self.multiworld.get_region("Menu",self.player) + menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) + + def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -128,13 +145,14 @@ def create_regions(self) -> None: # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item in chunksanity_special_region_names: - starting_area_region = chunksanity_special_region_names[self.starting_area_item] - else: - starting_area_region = self.starting_area_item[6:] # len("Area: ") - starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") - starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) - starting_entrance.connect(self.region_name_to_data[starting_area_region]) + if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) # Create entrances between regions for region_row in region_rows: From fced9050a477d0d66d0342a405b71987ec5bc3be Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 18 Sep 2024 12:09:47 -0700 Subject: [PATCH 286/393] Zillion: fix logic cache (#3719) --- worlds/zillion/__init__.py | 27 ++++-------- worlds/zillion/logic.py | 85 ++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cf61d93ca4ce..d5e86bb33292 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import settings import threading import typing -from typing import Any, Dict, List, Set, Tuple, Optional +from typing import Any, Dict, List, Set, Tuple, Optional, Union import os import logging @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .gen_data import GenData -from .logic import cs_to_zz_locs +from .logic import ZillionLogicCache from .region import ZillionLocation, ZillionRegion from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ @@ -21,7 +21,6 @@ from .item import ZillionItem from .patch import ZillionPatch -from zilliandomizer.randomizer import Randomizer as ZzRandomizer from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req @@ -121,6 +120,7 @@ def flush(self) -> None: """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ slot_data_ready: threading.Event """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ + logic_cache: Union[ZillionLogicCache, None] = None def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) @@ -134,9 +134,6 @@ def _make_item_maps(self, start_char: Chars) -> None: self.id_to_zz_item = id_to_zz_item def generate_early(self) -> None: - if not hasattr(self.multiworld, "zillion_logic_cache"): - setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.options) if zz_op.early_scope: @@ -163,6 +160,8 @@ def create_regions(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" assert self.id_to_zz_item, "generate_early hasn't been called" p = self.player + logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item) + self.logic_cache = logic_cache w = self.multiworld self.my_locations = [] @@ -201,15 +200,12 @@ def create_regions(self) -> None: if not zz_loc.item: def access_rule_wrapped(zz_loc_local: ZzLocation, - p: int, - zz_r: ZzRandomizer, - id_to_zz_item: Dict[int, ZzItem], + lc: ZillionLogicCache, cs: CollectionState) -> bool: - accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item) + accessible = lc.cs_to_zz_locs(cs) return zz_loc_local in accessible - access_rule = functools.partial(access_rule_wrapped, - zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item) + access_rule = functools.partial(access_rule_wrapped, zz_loc, logic_cache) loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] loc = ZillionLocation(zz_loc, self.player, loc_name, here) @@ -402,13 +398,6 @@ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot game = self.zz_system.get_game() return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) - # def modify_multidata(self, multidata: Dict[str, Any]) -> None: - # """For deeper modification of server multidata.""" - # # not modifying multidata, just want to call this at the end of the generation process - # cache = getattr(self.multiworld, "zillion_logic_cache") - # import sys - # print(sys.getsizeof(cache)) - # end of ordered Main.py calls def create_item(self, name: str) -> Item: diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index dcbc6131f1a9..a14910a200e5 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,4 +1,4 @@ -from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter +from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter from BaseClasses import CollectionState @@ -44,38 +44,51 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) -LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]] -""" { hash: (cs.prog_items, accessible_locations) } """ - - -def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: - """ - given an Archipelago `CollectionState`, - returns frozenset of accessible zilliandomizer locations - """ - # caching this function because it would be slow - logic_cache: LogicCacheType = getattr(cs.multiworld, "zillion_logic_cache", {}) - _hash = set_randomizer_locs(cs, p, zz_r) - counts = item_counts(cs, p) - _hash += hash(counts) - - if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items: - # print("cache hit") - return logic_cache[_hash][1] - - # print("cache miss") - have_items: List[Item] = [] - for name, count in counts: - have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count) - # have_req is the result of converting AP CollectionState to zilliandomizer collection state - have_req = zz_r.make_ability(have_items) - - # This `get_locations` is where the core of the logic comes in. - # It takes a zilliandomizer collection state (a set of the abilities that I have) - # and returns list of all the zilliandomizer locations I can access with those abilities. - tr = frozenset(zz_r.get_locations(have_req)) - - # save result in cache - logic_cache[_hash] = (cs.prog_items.copy(), tr) - - return tr +_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset()) + + +class ZillionLogicCache: + _cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]] + """ `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """ + _player: int + _zz_r: Randomizer + _id_to_zz_item: Mapping[int, Item] + + def __init__(self, player: int, zz_r: Randomizer, id_to_zz_item: Mapping[int, Item]) -> None: + self._cache = {} + self._player = player + self._zz_r = zz_r + self._id_to_zz_item = id_to_zz_item + + def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]: + """ + given an Archipelago `CollectionState`, + returns frozenset of accessible zilliandomizer locations + """ + # caching this function because it would be slow + _hash = set_randomizer_locs(cs, self._player, self._zz_r) + counts = item_counts(cs, self._player) + _hash += hash(counts) + + cntr, locs = self._cache.get(_hash, _cache_miss) + if cntr == cs.prog_items[self._player]: + # print("cache hit") + return locs + + # print("cache miss") + have_items: List[Item] = [] + for name, count in counts: + have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count) + # have_req is the result of converting AP CollectionState to zilliandomizer collection state + have_req = self._zz_r.make_ability(have_items) + # print(f"{have_req=}") + + # This `get_locations` is where the core of the logic comes in. + # It takes a zilliandomizer collection state (a set of the abilities that I have) + # and returns list of all the zilliandomizer locations I can access with those abilities. + tr = frozenset(self._zz_r.get_locations(have_req)) + + # save result in cache + self._cache[_hash] = (cs.prog_items[self._player].copy(), tr) + + return tr From 025c5509916158d19ee22ee884754c56ab8958c0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:26:59 -0500 Subject: [PATCH 287/393] Ocarina of Time: options and general cleanup (#3767) * working? * missed one * fix old start inventory usage * missed global random usage --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/oot/Cosmetics.py | 41 ++++---- worlds/oot/Entrance.py | 4 +- worlds/oot/EntranceShuffle.py | 40 ++++---- worlds/oot/HintList.py | 24 ++--- worlds/oot/Hints.py | 66 ++++++------ worlds/oot/ItemPool.py | 46 ++++----- worlds/oot/Messages.py | 5 +- worlds/oot/Music.py | 17 ++-- worlds/oot/N64Patch.py | 5 +- worlds/oot/Options.py | 185 ++++++++++++++++++++++++++++++---- worlds/oot/Patches.py | 26 ++--- worlds/oot/RuleParser.py | 28 ++--- worlds/oot/Rules.py | 18 ++-- worlds/oot/TextBox.py | 2 +- worlds/oot/__init__.py | 86 ++++++++-------- 15 files changed, 367 insertions(+), 226 deletions(-) diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py index f40f8a1ebb06..4a748c60aa9e 100644 --- a/worlds/oot/Cosmetics.py +++ b/worlds/oot/Cosmetics.py @@ -1,9 +1,9 @@ from .Utils import data_path, __version__ from .Colors import * import logging -import worlds.oot.Music as music -import worlds.oot.Sounds as sfx -import worlds.oot.IconManip as icon +from . import Music as music +from . import Sounds as sfx +from . import IconManip as icon from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict import json @@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols): # handle random if tunic_option == 'Random Choice': - tunic_option = random.choice(tunic_color_list) + tunic_option = ootworld.random.choice(tunic_color_list) # handle completely random if tunic_option == 'Completely Random': color = generate_random_color() @@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols): # choose a random choice for the whole group if navi_option_inner == 'Random Choice': - navi_option_inner = random.choice(navi_color_list) + navi_option_inner = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Random Choice': - navi_option_outer = random.choice(navi_color_list) + navi_option_outer = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Match Inner': navi_option_outer = navi_option_inner @@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(sword_trail_color_list) + option_inner = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(sword_trail_color_list) + option_outer = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(trail_color_list) + option_inner = ootworld.random.choice(trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(trail_color_list) + option_outer = ootworld.random.choice(trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols): # handle random if gauntlet_option == 'Random Choice': - gauntlet_option = random.choice(gauntlet_color_list) + gauntlet_option = ootworld.random.choice(gauntlet_color_list) # handle completely random if gauntlet_option == 'Completely Random': color = generate_random_color() @@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols): # handle random if shield_frame_option == 'Random Choice': - shield_frame_option = random.choice(shield_frame_color_list) + shield_frame_option = ootworld.random.choice(shield_frame_color_list) # handle completely random if shield_frame_option == 'Completely Random': - color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)] + color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)] # grab the color from the list elif shield_frame_option in shield_frame_colors: color = list(shield_frame_colors[shield_frame_option]) @@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols): # handle random if heart_option == 'Random Choice': - heart_option = random.choice(heart_color_list) + heart_option = ootworld.random.choice(heart_color_list) # handle completely random if heart_option == 'Completely Random': color = generate_random_color() @@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols): magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting]) if magic_option == 'Random Choice': - magic_option = random.choice(magic_color_list) + magic_option = ootworld.random.choice(magic_color_list) if magic_option == 'Completely Random': color = generate_random_color() @@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols): # handle random if button_option == 'Random Choice': - button_option = random.choice(list(button_colors.keys())) + button_option = ootworld.random.choice(list(button_colors.keys())) # handle completely random if button_option == 'Completely Random': fixed_font_color = [10, 10, 10] @@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols): rom.write_int16(loc, sound_id) else: if selection == 'random-choice': - selection = random.choice(sfx.get_hook_pool(hook)).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword elif selection == 'random-ear-safe': - selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword elif selection == 'completely-random': - selection = random.choice(sfx.standard).value.keyword + selection = ootworld.random.choice(sfx.standard).value.keyword sound_id = sound_dict[selection] for loc in hook.value.locations: rom.write_int16(loc, sound_id) @@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols): choice = ootworld.sfx_ocarina if choice == 'random-choice': - choice = random.choice(list(instruments.keys())) + choice = ootworld.random.choice(list(instruments.keys())) rom.write_byte(0x00B53C7B, instruments[choice]) rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods @@ -769,7 +769,6 @@ def patch_instrument(rom, ootworld, symbols): def patch_cosmetics(ootworld, rom): # Use the world's slot seed for cosmetics - random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random()) # try to detect the cosmetic patch data format versioned_patch_set = None diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index 6c4b6428f53e..8b041f045dcf 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -3,9 +3,9 @@ class OOTEntrance(Entrance): game: str = 'Ocarina of Time' - def __init__(self, player, world, name='', parent=None): + def __init__(self, player, multiworld, name='', parent=None): super(OOTEntrance, self).__init__(player, name, parent) - self.multiworld = world + self.multiworld = multiworld self.access_rules = [] self.reverse = None self.replaces = None diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index cda442ffb109..66c5df804cb4 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -440,16 +440,16 @@ class EntranceShuffleError(Exception): def shuffle_random_entrances(ootworld): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player # Gather locations to keep reachable for validation all_state = ootworld.get_state_with_complete_itempool() all_state.sweep_for_advancements(locations=ootworld.get_locations()) - locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} + locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances - set_all_entrances_data(world, player) + set_all_entrances_data(multiworld, player) # Determine entrance pools based on settings one_way_entrance_pools = {} @@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld): none_state = CollectionState(ootworld.multiworld) # Plando entrances - if world.plando_connections[player]: + if ootworld.options.plando_connections: rollbacks = [] all_targets = {**one_way_target_entrance_pools, **target_entrance_pools} - for conn in world.plando_connections[player]: + for conn in ootworld.options.plando_connections: try: entrance = ootworld.get_entrance(conn.entrance) exit = ootworld.get_entrance(conn.exit) @@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable new_all_state = ootworld.get_state_with_complete_itempool() - if not world.has_beaten_game(new_all_state, player): + if not multiworld.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state) @@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools): avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools)) - ootworld.multiworld.random.shuffle(avail_pool) + ootworld.random.shuffle(avail_pool) for entrance in avail_pool: if entrance.replaces: @@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}') def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): - ootworld.multiworld.random.shuffle(entrances) + ootworld.random.shuffle(entrances) for entrance in entrances: if entrance.connected_region != None: continue - ootworld.multiworld.random.shuffle(target_entrances) + ootworld.random.shuffle(target_entrances) # Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems. # success rate over randomization if pool_type in {'InteriorSoft', 'MixedSoft'}: @@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran # TODO: improve this function def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player all_state = all_state_orig.copy() @@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints - potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) - potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) + potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) + potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back): raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area') elif (potion_front and not potion_back) or (not potion_front and potion_back): @@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all # When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides if ootworld.shuffle_cows: - impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) - impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) + impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) + impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back): raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area') elif (impas_front and not impas_back) or (not impas_front and impas_back): @@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)): raise EntranceShuffleError('Time passing is not guaranteed as both ages') - if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): + if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as adult not guaranteed') - if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): + if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as child not guaranteed') if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']): # Ensure big poe shop is always reachable as adult - if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: + if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult') if ootworld.shopsanity == 'off': # Ensure that Goron and Zora shops are accessible as adult - if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Goron City Shop not accessible as adult') - if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult') if ootworld.open_forest == 'closed': # Ensure that Kokiri Shop is reachable as child with no items - if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: + if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest') diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index b0f20858e747..28a5d37a516a 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -1,5 +1,3 @@ -import random - from BaseClasses import LocationProgressType from .Items import OOTItem @@ -28,7 +26,7 @@ class Hint(object): text = "" type = [] - def __init__(self, name, text, type, choice=None): + def __init__(self, name, text, type, rand, choice=None): self.name = name self.type = [type] if not isinstance(type, list) else type @@ -36,31 +34,31 @@ def __init__(self, name, text, type, choice=None): self.text = text else: if choice == None: - self.text = random.choice(text) + self.text = rand.choice(text) else: self.text = text[choice] -def getHint(item, clearer_hint=False): +def getHint(item, rand, clearer_hint=False): if item in hintTable: textOptions, clearText, hintType = hintTable[item] if clearer_hint: if clearText == None: - return Hint(item, textOptions, hintType, 0) - return Hint(item, clearText, hintType) + return Hint(item, textOptions, hintType, rand, 0) + return Hint(item, clearText, hintType, rand) else: - return Hint(item, textOptions, hintType) + return Hint(item, textOptions, hintType, rand) elif isinstance(item, str): - return Hint(item, item, 'generic') + return Hint(item, item, 'generic', rand) else: # is an Item - return Hint(item.name, item.hint_text, 'item') + return Hint(item.name, item.hint_text, 'item', rand) def getHintGroup(group, world): ret = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if hint.name in world.always_hints and group == 'always': hint.type = 'always' @@ -95,7 +93,7 @@ def getHintGroup(group, world): def getRequiredHints(world): ret = [] for name in hintTable: - hint = getHint(name) + hint = getHint(name, world.random) if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world): ret.append(hint) return ret @@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False): location_hints = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if any(item in hint.type for item in ['always', 'dual_always', diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index e63e135e5045..c01241d04832 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -136,13 +136,13 @@ def getItemGenericName(item): def isRestrictedDungeonItem(dungeon, item): if not isinstance(item, OOTItem): return False - if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon': + if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon': return item in dungeon.dungeon_items - if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon': + if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon': return item in dungeon.small_keys - if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon': + if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon': return item in dungeon.boss_key - if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon': + if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon': return item in dungeon.boss_key return False @@ -261,8 +261,8 @@ def filterTrailingSpace(text): '', ] -def getSimpleHintNoPrefix(item): - hint = getHint(item.name, True).text +def getSimpleHintNoPrefix(item, rand): + hint = getHint(item.name, rand, True).text for prefix in hintPrefixes: if hint.startswith(prefix): @@ -417,9 +417,9 @@ def is_dungeon_item(self, item): # Formats the hint text for this area with proper grammar. # Dungeons are hinted differently depending on the clearer_hints setting. - def text(self, clearer_hints, preposition=False, world=None): + def text(self, rand, clearer_hints, preposition=False, world=None): if self.is_dungeon: - text = getHint(self.dungeon_name, clearer_hints).text + text = getHint(self.dungeon_name, rand, clearer_hints).text else: text = str(self) prefix, suffix = text.replace('#', '').split(' ', 1) @@ -489,7 +489,7 @@ def get_woth_hint(world, checked): if getattr(location.parent_region, "dungeon", None): world.woth_dungeon += 1 - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text else: location_text = get_hint_area(location) @@ -570,9 +570,9 @@ def get_good_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text if world.hint_dist_user.get('vague_named_items', False): return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) else: @@ -648,9 +648,9 @@ def get_random_location_hint(world, checked): checked[location.player].add(location.name) dungeon = location.parent_region.dungeon - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if dungeon: - location_text = getHint(dungeon.name, world.clearer_hints).text + location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type): location_text = hint.text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) @@ -724,9 +724,9 @@ def get_entrance_hint(world, checked): connected_region = entrance.connected_region if connected_region.dungeon: - region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text + region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text else: - region_text = getHint(connected_region.name, world.clearer_hints).text + region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text if '#' not in region_text: region_text = '#%s#' % region_text @@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None): if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] else: - location_text = getHint(location.name, world.clearer_hints).text + location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) logging.getLogger('').debug('Placed always hint for %s.', location.name) @@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) ('Goron Ruby', 'Red'), ('Zora Sapphire', 'Blue'), ] - child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04' + child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04' for (reward, color) in bossRewardsSpiritualStones: child_text += buildBossString(reward, color, world) - child_text += getHint('Child Altar Text End', world.clearer_hints).text + child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text child_text += '\x0B' update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20) # text that appears at altar as an adult. adult_text = '\x08' - adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04' + adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04' if include_rewards: bossRewardsMedallions = [ ('Light Medallion', 'Light Blue'), @@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) adult_text += '\x04' adult_text += buildGanonBossKeyString(world) else: - adult_text += getHint('Adult Altar Text End', world.clearer_hints).text + adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text adult_text += '\x0B' update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20) @@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world): text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='') else: location = world.hinted_dungeon_reward_locations[reward] - location_text = HintArea.at(location).text(world.clearer_hints, preposition=True) + location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True) text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='') return str(text) + '\x04' @@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world): if world.bridge == 'open': string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells." else: - item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text + item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text if world.bridge == 'medallions': item_req_string = str(world.bridge_medallions) + ' ' + item_req_string elif world.bridge == 'stones': @@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world): string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#." else: if world.shuffle_ganon_bosskey == 'on_lacs': - item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text + item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text if world.lacs_condition == 'medallions': item_req_string = str(world.lacs_medallions) + ' ' + item_req_string elif world.lacs_condition == 'stones': @@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']: - item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text if world.shuffle_ganon_bosskey == 'medallions': item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string elif world.shuffle_ganon_bosskey == 'stones': @@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "automatically granted once %s are retrieved" % item_req_string else: - bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string return str(GossipText(string, ['Yellow'], prefix='')) @@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages): if location.player != world.player: player_text = world.multiworld.get_player_name(location.player) + "'s " if location.game == 'Ocarina of Time': - area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None) + area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None) else: area = location.name text = data['default_item_text'].format(area=rom_safe_text(player_text + area)) elif 'default_item_fallback' in data: text = data['default_item_fallback'] else: - text = getHint('Validation Line', world.clearer_hints).text + text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text location = world.get_location('Ganons Tower Boss Key Chest') - text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#" + text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#" for find, replace in data.get('replace', {}).items(): text = text.replace(find, replace) @@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages): if hint_type in world.misc_hints: location = world.get_location(data['item_location']) item = location.item - item_text = getHint(getItemGenericName(item), world.clearer_hints).text + item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text if item.player != world.player: item_text += f' for {world.multiworld.get_player_name(item.player)}' text = data['location_text'].format(item=rom_safe_text(item_text)) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 6ca6bc9268a9..805d1fc72dd2 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -295,16 +295,14 @@ def get_spec(tup, key, default): def get_junk_pool(ootworld): junk_pool[:] = list(junk_pool_base) - if ootworld.junk_ice_traps == 'on': + if ootworld.options.junk_ice_traps == 'on': junk_pool.append(('Ice Trap', 10)) - elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']: + elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']: junk_pool[:] = [('Ice Trap', 1)] return junk_pool -def get_junk_item(count=1, pool=None, plando_pool=None): - global random - +def get_junk_item(rand, count=1, pool=None, plando_pool=None): if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") @@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None): raise RuntimeError("Not enough junk is available in the item pool to replace removed items.") else: junk_items, junk_weights = zip(*junk_pool) - return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count)) + return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count)) return return_pool -def replace_max_item(items, item, max): +def replace_max_item(items, item, max, rand): count = 0 for i,val in enumerate(items): if val == item: if count >= max: - items[i] = get_junk_item()[0] + items[i] = get_junk_item(rand)[0] count += 1 @@ -375,7 +373,7 @@ def get_pool_core(world): pending_junk_pool.append('Kokiri Sword') if world.shuffle_ocarinas: pending_junk_pool.append('Ocarina') - if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0): + if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0): pending_junk_pool.append('Magic Bean Pack') if (world.gerudo_fortress != "open" and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']): @@ -450,7 +448,7 @@ def get_pool_core(world): else: item = deku_scrubs_items[location.vanilla_item] if isinstance(item, list): - item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] + item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] shuffle_item = True # Kokiri Sword @@ -489,7 +487,7 @@ def get_pool_core(world): # Cows elif location.vanilla_item == 'Milk': if world.shuffle_cows: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = world.shuffle_cows if not shuffle_item: location.show_in_spoiler = False @@ -508,13 +506,13 @@ def get_pool_core(world): item = 'Rutos Letter' ruto_bottles -= 1 else: - item = random.choice(normal_bottles) + item = world.random.choice(normal_bottles) shuffle_item = True # Magic Beans elif location.vanilla_item == 'Buy Magic Bean': if world.shuffle_beans: - item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0] + item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0] shuffle_item = world.shuffle_beans if not shuffle_item: location.show_in_spoiler = False @@ -528,7 +526,7 @@ def get_pool_core(world): # Adult Trade Item elif location.vanilla_item == 'Pocket Egg': potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items - item = random.choice(sorted(potential_trade_items)) + item = world.random.choice(sorted(potential_trade_items)) world.selected_adult_trade_item = item shuffle_item = True @@ -541,7 +539,7 @@ def get_pool_core(world): shuffle_item = False location.show_in_spoiler = False if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings: - item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' + item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' # Freestanding Rupees and Hearts elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']: @@ -618,7 +616,7 @@ def get_pool_core(world): elif dungeon.name in world.key_rings and not dungeon.small_keys: item = dungeon.item_name("Small Key Ring") elif dungeon.name in world.key_rings: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True # Any other item in a dungeon. elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]: @@ -630,7 +628,7 @@ def get_pool_core(world): if shuffle_setting in ['remove', 'startwith']: world.multiworld.push_precollected(dungeon_collection[-1]) world.remove_from_start_inventory.append(dungeon_collection[-1].name) - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']: dungeon_collection[-1].priority = True @@ -658,9 +656,9 @@ def get_pool_core(world): shop_non_item_count = len(world.shop_prices) shop_item_count = shop_slots_count - shop_non_item_count - pool.extend(random.sample(remain_shop_items, shop_item_count)) + pool.extend(world.random.sample(remain_shop_items, shop_item_count)) if shop_non_item_count: - pool.extend(get_junk_item(shop_non_item_count)) + pool.extend(get_junk_item(world.random, shop_non_item_count)) # Extra rupees for shopsanity. if world.shopsanity not in ['off', '0']: @@ -706,19 +704,19 @@ def get_pool_core(world): if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']: placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)' - pool.extend(get_junk_item()) + pool.extend(get_junk_item(world.random)) else: placed_items['Gift from Sages'] = IGNORE_LOCATION world.get_location('Gift from Sages').show_in_spoiler = False if world.junk_ice_traps == 'off': - replace_max_item(pool, 'Ice Trap', 0) + replace_max_item(pool, 'Ice Trap', 0, world.random) elif world.junk_ice_traps == 'onslaught': for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']: - replace_max_item(pool, item, 0) + replace_max_item(pool, item, 0, world.random) for item, maximum in item_difficulty_max[world.item_pool_value].items(): - replace_max_item(pool, item, maximum) + replace_max_item(pool, item, maximum, world.random) # world.distribution.alter_pool(world, pool) @@ -748,7 +746,7 @@ def get_pool_core(world): pending_item = pending_junk_pool.pop() if not junk_candidates: raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1)) - junk_item = random.choice(junk_candidates) + junk_item = world.random.choice(junk_candidates) junk_candidates.remove(junk_item) pool.remove(junk_item) pool.append(pending_item) diff --git a/worlds/oot/Messages.py b/worlds/oot/Messages.py index 25c2a9934dd4..5059c01f3c8d 100644 --- a/worlds/oot/Messages.py +++ b/worlds/oot/Messages.py @@ -1,6 +1,5 @@ # text details: https://wiki.cloudmodding.com/oot/Text_Format -import random from .HintList import misc_item_hint_table, misc_location_hint_table from .TextBox import line_wrap from .Utils import find_last @@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # shuffles the messages in the game, making sure to keep various message types in their own group -def shuffle_messages(messages, except_hints=True, always_allow_skip=True): +def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True): permutation = [i for i, _ in enumerate(messages)] @@ -1002,7 +1001,7 @@ def is_exempt(m): def shuffle_group(group): group_permutation = [i for i, _ in enumerate(group)] - random.shuffle(group_permutation) + rand.shuffle(group_permutation) for index_from, index_to in enumerate(group_permutation): permutation[group[index_to].index] = group[index_from].index diff --git a/worlds/oot/Music.py b/worlds/oot/Music.py index 6ed1ab54ae5d..1bb3b65aac3f 100644 --- a/worlds/oot/Music.py +++ b/worlds/oot/Music.py @@ -1,6 +1,5 @@ #Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer -import random import os from .Utils import compare_version, data_path @@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence return sequences, target_sequences -def shuffle_music(sequences, target_sequences, music_mapping, log): +def shuffle_music(sequences, target_sequences, music_mapping, log, rand): sequence_dict = {} sequence_ids = [] @@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log): # Shuffle the sequences if len(sequences) < len(target_sequences): raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).") - random.shuffle(sequence_ids) + rand.shuffle(sequence_ids) sequences = [] for target_sequence in target_sequences: @@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences): rom.write_byte(base, j.instrument_set) -def shuffle_pointers_table(rom, ids, music_mapping, log): +def shuffle_pointers_table(rom, ids, music_mapping, log, rand): # Read in all the Music data bgm_data = {} bgm_ids = [] @@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log): bgm_ids.append(bgm[0]) # shuffle data - random.shuffle(bgm_ids) + rand.shuffle(bgm_ids) # Write Music data back in random ordering for bgm in ids: @@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping): # process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids) # if ootworld.background_music == 'random_custom_only': # sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()] - # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log) + # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random) # if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped: # process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare') # if ootworld.fanfares == 'random_custom_only': # fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()] - # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log) + # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random) # if disabled_source_sequences: # log = disable_music(rom, disabled_source_sequences.values(), log) @@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping): # rebuild_sequences(rom, sequences + fanfare_sequences) # else: if ootworld.background_music == 'randomized' or bgm_mapped: - log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log) + log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random) if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped: - log = shuffle_pointers_table(rom, ff_ids, music_mapping, log) + log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random) # end_else if disabled_target_sequences: log = disable_music(rom, disabled_target_sequences.values(), log) diff --git a/worlds/oot/N64Patch.py b/worlds/oot/N64Patch.py index 5af3279e8077..3013a94a8e3b 100644 --- a/worlds/oot/N64Patch.py +++ b/worlds/oot/N64Patch.py @@ -1,5 +1,4 @@ import struct -import random import io import array import zlib @@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue): # xor_range is the range the XOR key will read from. This range is not # too important, but I tried to choose from a section that didn't really # have big gaps of 0s which we want to avoid. -def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): +def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)): dma_start, dma_end = rom.get_dma_table_range() # add header @@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): # get random xor key. This range is chosen because it generally # doesn't have many sections of 0s - xor_address = random.Random().randint(*xor_range) + xor_address = rand.randint(*xor_range) patch_data.append_int32(xor_address) new_buffer = copy.copy(rom.original.buffer) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index daf072adb59c..613c5d01b381 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,8 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \ + PerGameCommonOptions, OptionGroup from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -1281,21 +1283,166 @@ class LogicTricks(OptionList): valid_keys_casefold = True -# All options assembled into a single dict -oot_options: typing.Dict[str, type(Option)] = { - "plando_connections": OoTPlandoConnections, - "logic_rules": Logic, - "logic_no_night_tokens_without_suns_song": NightTokens, - **open_options, - **world_options, - **bridge_options, - **dungeon_items_options, - **shuffle_options, - **timesavers_options, - **misc_options, - **itempool_options, - **cosmetic_options, - **sfx_options, - "logic_tricks": LogicTricks, - "death_link": DeathLink, -} +@dataclass +class OoTOptions(PerGameCommonOptions): + plando_connections: OoTPlandoConnections + death_link: DeathLink + logic_rules: Logic + logic_no_night_tokens_without_suns_song: NightTokens + logic_tricks: LogicTricks + open_forest: Forest + open_kakariko: Gate + open_door_of_time: DoorOfTime + zora_fountain: Fountain + gerudo_fortress: Fortress + bridge: Bridge + trials: Trials + starting_age: StartingAge + shuffle_interior_entrances: InteriorEntrances + shuffle_grotto_entrances: GrottoEntrances + shuffle_dungeon_entrances: DungeonEntrances + shuffle_overworld_entrances: OverworldEntrances + owl_drops: OwlDrops + warp_songs: WarpSongs + spawn_positions: SpawnPositions + shuffle_bosses: BossEntrances + # mix_entrance_pools: MixEntrancePools + # decouple_entrances: DecoupleEntrances + triforce_hunt: TriforceHunt + triforce_goal: TriforceGoal + extra_triforce_percentage: ExtraTriforces + bombchus_in_logic: LogicalChus + dungeon_shortcuts: DungeonShortcuts + dungeon_shortcuts_list: DungeonShortcutsList + mq_dungeons_mode: MQDungeons + mq_dungeons_list: MQDungeonList + mq_dungeons_count: MQDungeonCount + # empty_dungeons_mode: EmptyDungeons + # empty_dungeons_list: EmptyDungeonList + # empty_dungeon_count: EmptyDungeonCount + bridge_stones: BridgeStones + bridge_medallions: BridgeMedallions + bridge_rewards: BridgeRewards + bridge_tokens: BridgeTokens + bridge_hearts: BridgeHearts + shuffle_mapcompass: ShuffleMapCompass + shuffle_smallkeys: ShuffleKeys + shuffle_hideoutkeys: ShuffleGerudoKeys + shuffle_bosskeys: ShuffleBossKeys + enhance_map_compass: EnhanceMC + shuffle_ganon_bosskey: ShuffleGanonBK + ganon_bosskey_medallions: GanonBKMedallions + ganon_bosskey_stones: GanonBKStones + ganon_bosskey_rewards: GanonBKRewards + ganon_bosskey_tokens: GanonBKTokens + ganon_bosskey_hearts: GanonBKHearts + key_rings: KeyRings + key_rings_list: KeyRingList + shuffle_song_items: SongShuffle + shopsanity: ShopShuffle + shop_slots: ShopSlots + shopsanity_prices: ShopPrices + tokensanity: TokenShuffle + shuffle_scrubs: ScrubShuffle + shuffle_child_trade: ShuffleChildTrade + shuffle_freestanding_items: ShuffleFreestanding + shuffle_pots: ShufflePots + shuffle_crates: ShuffleCrates + shuffle_cows: ShuffleCows + shuffle_beehives: ShuffleBeehives + shuffle_kokiri_sword: ShuffleSword + shuffle_ocarinas: ShuffleOcarinas + shuffle_gerudo_card: ShuffleCard + shuffle_beans: ShuffleBeans + shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet + shuffle_frog_song_rupees: ShuffleFrogRupees + no_escape_sequence: SkipEscape + no_guard_stealth: SkipStealth + no_epona_race: SkipEponaRace + skip_some_minigame_phases: SkipMinigamePhases + complete_mask_quest: CompleteMaskQuest + useful_cutscenes: UsefulCutscenes + fast_chests: FastChests + free_scarecrow: FreeScarecrow + fast_bunny_hood: FastBunny + plant_beans: PlantBeans + chicken_count: ChickenCount + big_poe_count: BigPoeCount + fae_torch_count: FAETorchCount + correct_chest_appearances: CorrectChestAppearance + minor_items_as_major_chest: MinorInMajor + invisible_chests: InvisibleChests + correct_potcrate_appearances: CorrectPotCrateAppearance + hints: Hints + misc_hints: MiscHints + hint_dist: HintDistribution + text_shuffle: TextShuffle + damage_multiplier: DamageMultiplier + deadly_bonks: DeadlyBonks + no_collectible_hearts: HeroMode + starting_tod: StartingToD + blue_fire_arrows: BlueFireArrows + fix_broken_drops: FixBrokenDrops + start_with_consumables: ConsumableStart + start_with_rupees: RupeeStart + item_pool_value: ItemPoolValue + junk_ice_traps: IceTraps + ice_trap_appearance: IceTrapVisual + adult_trade_start: AdultTradeStart + default_targeting: Targeting + display_dpad: DisplayDpad + dpad_dungeon_menu: DpadDungeonMenu + correct_model_colors: CorrectColors + background_music: BackgroundMusic + fanfares: Fanfares + ocarina_fanfares: OcarinaFanfares + kokiri_color: kokiri_color + goron_color: goron_color + zora_color: zora_color + silver_gauntlets_color: silver_gauntlets_color + golden_gauntlets_color: golden_gauntlets_color + mirror_shield_frame_color: mirror_shield_frame_color + navi_color_default_inner: navi_color_default_inner + navi_color_default_outer: navi_color_default_outer + navi_color_enemy_inner: navi_color_enemy_inner + navi_color_enemy_outer: navi_color_enemy_outer + navi_color_npc_inner: navi_color_npc_inner + navi_color_npc_outer: navi_color_npc_outer + navi_color_prop_inner: navi_color_prop_inner + navi_color_prop_outer: navi_color_prop_outer + sword_trail_duration: SwordTrailDuration + sword_trail_color_inner: sword_trail_color_inner + sword_trail_color_outer: sword_trail_color_outer + bombchu_trail_color_inner: bombchu_trail_color_inner + bombchu_trail_color_outer: bombchu_trail_color_outer + boomerang_trail_color_inner: boomerang_trail_color_inner + boomerang_trail_color_outer: boomerang_trail_color_outer + heart_color: heart_color + magic_color: magic_color + a_button_color: a_button_color + b_button_color: b_button_color + c_button_color: c_button_color + start_button_color: start_button_color + sfx_navi_overworld: sfx_navi_overworld + sfx_navi_enemy: sfx_navi_enemy + sfx_low_hp: sfx_low_hp + sfx_menu_cursor: sfx_menu_cursor + sfx_menu_select: sfx_menu_select + sfx_nightfall: sfx_nightfall + sfx_horse_neigh: sfx_horse_neigh + sfx_hover_boots: sfx_hover_boots + sfx_ocarina: SfxOcarina + + +oot_option_groups: typing.List[OptionGroup] = [ + OptionGroup("Open", [option for option in open_options.values()]), + OptionGroup("World", [*[option for option in world_options.values()], + *[option for option in bridge_options.values()]]), + OptionGroup("Shuffle", [option for option in shuffle_options.values()]), + OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]), + OptionGroup("Timesavers", [option for option in timesavers_options.values()]), + OptionGroup("Misc", [option for option in misc_options.values()]), + OptionGroup("Item Pool", [option for option in itempool_options.values()]), + OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]), + OptionGroup("SFX", [option for option in sfx_options.values()]) +] diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 2219d7bb95a8..561d7c3f7b6e 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -208,8 +208,8 @@ def patch_rom(world, rom): # Fix Ice Cavern Alcove Camera if not world.dungeon_mq['Ice Cavern']: - rom.write_byte(0x2BECA25,0x01); - rom.write_byte(0x2BECA2D,0x01); + rom.write_byte(0x2BECA25,0x01) + rom.write_byte(0x2BECA2D,0x01) # Fix GS rewards to be static rom.write_int32(0xEA3934, 0) @@ -944,7 +944,7 @@ def add_scene_exits(scene_start, offset = 0): scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_start = rom.read_int32(scene_table + (scene * 0x14)); + scene_start = rom.read_int32(scene_table + (scene * 0x14)) add_scene_exits(scene_start) return exit_table @@ -1632,10 +1632,10 @@ def set_entrance_updates(entrances): reward_text = None elif getattr(location.item, 'looks_like_item', None) is not None: jabu_item = location.item.looks_like_item - reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text) + reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text) else: jabu_item = location.item - reward_text = getHint(getItemGenericName(location.item), True).text + reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text # Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu if reward_text is None: @@ -1687,7 +1687,7 @@ def set_entrance_updates(entrances): # Sets hooks for gossip stone changes - symbol = rom.sym("GOSSIP_HINT_CONDITION"); + symbol = rom.sym("GOSSIP_HINT_CONDITION") if world.hints == 'none': rom.write_int32(symbol, 0) @@ -2264,9 +2264,9 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # text shuffle if world.text_shuffle == 'except_hints': - permutation = shuffle_messages(messages, except_hints=True) + permutation = shuffle_messages(messages, world.random, except_hints=True) elif world.text_shuffle == 'complete': - permutation = shuffle_messages(messages, except_hints=False) + permutation = shuffle_messages(messages, world.random, except_hints=False) # update warp song preview text boxes update_warp_song_text(messages, world) @@ -2358,7 +2358,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # Write numeric seed truncated to 32 bits for rng seeding # Overwritten with new seed every time a new rng value is generated - rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32) + rng_seed = world.random.getrandbits(32) rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed) # Static initial seed value for one-time random actions like the Hylian Shield discount rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed) @@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process room_count = rom.read_byte(scene_data + 1) room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF) for _ in range(0, room_count): - room_data = rom.read_int32(room_list); + room_data = rom.read_int32(room_list) if not room_data in processed_rooms: actors.update(room_get_actors(rom, actor_func, room_data, scene)) @@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func): actors = {} scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_data = rom.read_int32(scene_table + (scene * 0x14)); + scene_data = rom.read_int32(scene_table + (scene * 0x14)) actors.update(scene_get_actors(rom, actor_func, scene_data, scene)) return actors @@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags): def remove_entrance_blockers(rom): def remove_entrance_blockers_do(rom, actor_id, actor, scene): if actor_id == 0x014E and scene == 97: - actor_var = rom.read_int16(actor + 14); + actor_var = rom.read_int16(actor + 14) if actor_var == 0xFF01: rom.write_int16(actor + 14, 0x0700) get_actor_list(rom, remove_entrance_blockers_do) @@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1]) else: if item_display.game == "Ocarina of Time": - shop_item_name = getSimpleHintNoPrefix(item_display) + shop_item_name = getSimpleHintNoPrefix(item_display, world.random) else: shop_item_name = item_display.name diff --git a/worlds/oot/RuleParser.py b/worlds/oot/RuleParser.py index 0791ad5d1a3f..e5390474b779 100644 --- a/worlds/oot/RuleParser.py +++ b/worlds/oot/RuleParser.py @@ -53,7 +53,7 @@ def isliteral(expr): class Rule_AST_Transformer(ast.NodeTransformer): def __init__(self, world, player): - self.multiworld = world + self.world = world self.player = player self.events = set() # map Region -> rule ast string -> item name @@ -86,9 +86,9 @@ def visit_Name(self, node): ctx=ast.Load()), args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)], keywords=[]) - elif node.id in self.multiworld.__dict__: + elif node.id in self.world.__dict__: # Settings are constant - return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body + return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body elif node.id in State.__dict__: return self.make_call(node, node.id, [], []) elif node.id in self.kwarg_defaults or node.id in allowed_globals: @@ -137,7 +137,7 @@ def visit_Tuple(self, node): if isinstance(count, ast.Name): # Must be a settings constant - count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body + count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body if iname in escaped_items: iname = escaped_items[iname] @@ -182,7 +182,7 @@ def visit_Call(self, node): new_args = [] for child in node.args: if isinstance(child, ast.Name): - if child.id in self.multiworld.__dict__: + if child.id in self.world.__dict__: # child = ast.Attribute( # value=ast.Attribute( # value=ast.Name(id='state', ctx=ast.Load()), @@ -190,7 +190,7 @@ def visit_Call(self, node): # ctx=ast.Load()), # attr=child.id, # ctx=ast.Load()) - child = ast.Constant(getattr(self.multiworld, child.id)) + child = ast.Constant(getattr(self.world, child.id)) elif child.id in rule_aliases: child = self.visit(child) elif child.id in escaped_items: @@ -242,7 +242,7 @@ def escape_or_string(n): # Fast check for json can_use if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq) and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name) - and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__): + and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__): return ast.NameConstant(node.left.id == node.comparators[0].id) node.left = escape_or_string(node.left) @@ -378,7 +378,7 @@ def replace_subrule(self, target, node): # Requires the target regions have been defined in the world. def create_delayed_rules(self): for region_name, node, subrule_name in self.delayed_rules: - region = self.multiworld.multiworld.get_region(region_name, self.player) + region = self.world.multiworld.get_region(region_name, self.player) event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True) event.show_in_spoiler = False @@ -395,7 +395,7 @@ def create_delayed_rules(self): set_rule(event, access_rule) region.locations.append(event) - self.multiworld.make_event_item(subrule_name, event) + self.world.make_event_item(subrule_name, event) # Safeguard in case this is called multiple times per world self.delayed_rules.clear() @@ -448,7 +448,7 @@ def here(self, node): ## Handlers for compile-time optimizations (former State functions) def at_day(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAY or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -456,7 +456,7 @@ def at_day(self, node): return ast.NameConstant(True) def at_dampe_time(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -464,10 +464,10 @@ def at_dampe_time(self, node): return ast.NameConstant(True) def at_night(self, node): - if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song: + if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song: # Using visit here to resolve 'can_play' rule return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body) - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -501,7 +501,7 @@ def current_spot_adult_access(self, node): return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body def current_spot_starting_age_access(self, node): - return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node) + return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node) def has_bottle(self, node): return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 36563a3f9f27..00f4aeb4b7d5 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -10,7 +10,7 @@ from BaseClasses import CollectionState, MultiWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class OOTLogic(LogicMixin): @@ -132,17 +132,17 @@ def _oot_update_age_reachable_regions(self, player): def set_rules(ootworld): logger = logging.getLogger('') - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player if ootworld.logic_rules != 'no_logic': if ootworld.triforce_hunt: - world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) + multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) else: - world.completion_condition[player] = lambda state: state.has('Triforce', player) + multiworld.completion_condition[player] = lambda state: state.has('Triforce', player) # ganon can only carry triforce - world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' + multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' # is_child = ootworld.parser.parse_rule('is_child') guarantee_hint = ootworld.parser.parse_rule('guarantee_hint') @@ -156,22 +156,22 @@ def set_rules(ootworld): if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'): # First room chest needs to be a small key. Make sure the boss key isn't placed here. - location = world.get_location('Forest Temple MQ First Room Chest', player) + location = multiworld.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. - location = world.get_location('Sheik in Ice Cavern', player) + location = multiworld.get_location('Sheik in Ice Cavern', player) add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) if ootworld.shuffle_child_trade == 'skip_child_zelda': # Song from Impa must be local - location = world.get_location('Song from Impa', player) + location = multiworld.get_location('Song from Impa', player) add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: - add_rule(world.get_location(name, player), guarantee_hint) + add_rule(multiworld.get_location(name, player), guarantee_hint) # TODO: re-add hints once they are working # if location.type == 'HintStone' and ootworld.hints == 'mask': diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py index a9db47996299..e502d739048f 100644 --- a/worlds/oot/TextBox.py +++ b/worlds/oot/TextBox.py @@ -1,4 +1,4 @@ -import worlds.oot.Messages as Messages +from . import Messages # Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the # characters on a line reach this value. diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 94587a41a0f2..b93f60b2a08e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -20,7 +20,7 @@ from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .RuleParser import Rule_AST_Transformer -from .Options import oot_options +from .Options import OoTOptions, oot_option_groups from .Utils import data_path, read_json from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations from .DungeonList import dungeon_table, create_dungeons @@ -30,12 +30,12 @@ from .N64Patch import create_patch_file from .Cosmetics import patch_cosmetics -from Utils import get_options +from settings import get_settings from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule -from ..AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.AutoWorld import World, AutoLogicRegister, WebWorld # OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory. i_o_limiter = threading.Semaphore(2) @@ -128,6 +128,7 @@ class OOTWeb(WebWorld): ) tutorials = [setup, setup_es, setup_fr, setup_de] + option_groups = oot_option_groups class OOTWorld(World): @@ -137,7 +138,8 @@ class OOTWorld(World): to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! """ game: str = "Ocarina of Time" - option_definitions: dict = oot_options + options_dataclass = OoTOptions + options: OoTOptions settings: typing.ClassVar[OOTSettings] topology_present: bool = True item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if @@ -195,15 +197,15 @@ def __init__(self, world, player): @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) - for (option_name, option) in oot_options.items(): - result = getattr(self.multiworld, option_name)[self.player] + for option_name in self.options_dataclass.type_hints: + result = getattr(self.options, option_name) if isinstance(result, Range): option_value = int(result) elif isinstance(result, Toggle): @@ -223,8 +225,8 @@ def generate_early(self): self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False - self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)] - self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16)) + self.file_hash = [self.random.randint(0, 31) for i in range(5)] + self.connect_name = ''.join(self.random.choices(printable, k=16)) self.collectible_flag_addresses = {} # Incompatible option handling @@ -283,7 +285,7 @@ def generate_early(self): local_types.append('BossKey') if self.shuffle_ganon_bosskey != 'keysanity': local_types.append('GanonBossKey') - self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types) + self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types) # If any songs are itemlinked, set songs_as_items for group in self.multiworld.groups.values(): @@ -297,7 +299,7 @@ def generate_early(self): # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] - chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip + chosen_trials = self.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list} # Determine tricks in logic @@ -311,8 +313,8 @@ def generate_early(self): # No Logic forces all tricks on, prog balancing off and beatable-only elif self.logic_rules == 'no_logic': - self.multiworld.progression_balancing[self.player].value = False - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.progression_balancing.value = False + self.options.accessibility.value = Accessibility.option_minimal for trick in normalized_name_tricks.values(): setattr(self, trick['name'], True) @@ -333,8 +335,8 @@ def generate_early(self): # Set internal names used by the OoT generator self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] - self.trials_random = self.multiworld.trials[self.player].randomized - self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized + self.trials_random = self.options.trials.randomized + self.mq_dungeons_random = self.options.mq_dungeons_count.randomized self.easier_fire_arrow_entry = self.fae_torch_count < 24 if self.misc_hints: @@ -393,8 +395,8 @@ def generate_early(self): elif self.key_rings == 'choose': self.key_rings = self.key_rings_list elif self.key_rings == 'random_dungeons': - self.key_rings = self.multiworld.random.sample(keyring_dungeons, - self.multiworld.random.randint(0, len(keyring_dungeons))) + self.key_rings = self.random.sample(keyring_dungeons, + self.random.randint(0, len(keyring_dungeons))) # Determine which dungeons are MQ. Not compatible with glitched logic. mq_dungeons = set() @@ -405,7 +407,7 @@ def generate_early(self): elif self.mq_dungeons_mode == 'specific': mq_dungeons = self.mq_dungeons_specific elif self.mq_dungeons_mode == 'count': - mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count) + mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count) else: self.mq_dungeons_mode = 'count' self.mq_dungeons_count = 0 @@ -425,8 +427,8 @@ def generate_early(self): elif self.dungeon_shortcuts_choice == 'all': self.dungeon_shortcuts = set(shortcut_dungeons) elif self.dungeon_shortcuts_choice == 'random': - self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons, - self.multiworld.random.randint(0, len(shortcut_dungeons))) + self.dungeon_shortcuts = self.random.sample(shortcut_dungeons, + self.random.randint(0, len(shortcut_dungeons))) # == 'choice', leave as previous else: self.dungeon_shortcuts = set() @@ -576,7 +578,7 @@ def load_regions_from_json(self, file_path): new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region) new_exit.vanilla_connected_region = exit new_exit.rule_string = rule - if self.multiworld.logic_rules != 'none': + if self.options.logic_rules != 'no_logic': self.parser.parse_spot_rule(new_exit) if new_exit.never: logger.debug('Dropping unreachable exit: %s', new_exit.name) @@ -607,7 +609,7 @@ def set_scrub_prices(self): elif self.shuffle_scrubs == 'random': # this is a random value between 0-99 # average value is ~33 rupees - price = int(self.multiworld.random.betavariate(1, 2) * 99) + price = int(self.random.betavariate(1, 2) * 99) # Set price in the dictionary as well as the location. self.scrub_prices[scrub_item] = price @@ -624,7 +626,7 @@ def random_shop_prices(self): self.shop_prices = {} for region in self.regions: if self.shopsanity == 'random': - shop_item_count = self.multiworld.random.randint(0, 4) + shop_item_count = self.random.randint(0, 4) else: shop_item_count = int(self.shopsanity) @@ -632,17 +634,17 @@ def random_shop_prices(self): if location.type == 'Shop': if location.name[-1:] in shop_item_indexes[:shop_item_count]: if self.shopsanity_prices == 'normal': - self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5 + self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5 elif self.shopsanity_prices == 'affordable': self.shop_prices[location.name] = 10 elif self.shopsanity_prices == 'starting_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5) + self.shop_prices[location.name] = self.random.randrange(0,100,5) elif self.shopsanity_prices == 'adults_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5) + self.shop_prices[location.name] = self.random.randrange(0,201,5) elif self.shopsanity_prices == 'giants_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5) + self.shop_prices[location.name] = self.random.randrange(0,501,5) elif self.shopsanity_prices == 'tycoons_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + self.shop_prices[location.name] = self.random.randrange(0,1000,5) # Fill boss prizes @@ -667,8 +669,8 @@ def fill_bosses(self, bossCount=9): while bossCount: bossCount -= 1 - self.multiworld.random.shuffle(prizepool) - self.multiworld.random.shuffle(prize_locs) + self.random.shuffle(prizepool) + self.random.shuffle(prize_locs) item = prizepool.pop() loc = prize_locs.pop() loc.place_locked_item(item) @@ -778,7 +780,7 @@ def create_items(self): # Call the junk fill and get a replacement if item in self.itempool: self.itempool.remove(item) - self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) + self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool))) if self.start_with_consumables: self.starting_items['Deku Sticks'] = 30 self.starting_items['Deku Nuts'] = 40 @@ -881,7 +883,7 @@ def prefill_state(base_state): # Prefill shops, songs, and dungeon items items = self.get_pre_fill_items() locations = list(self.multiworld.get_unfilled_locations(self.player)) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) # Set up initial state state = CollectionState(self.multiworld) @@ -910,7 +912,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in stage_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, single_player_placement=True, lock=True, allow_excluded=True) else: @@ -923,7 +925,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in dungeon_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) @@ -964,7 +966,7 @@ def prefill_state(base_state): while tries: try: - self.multiworld.random.shuffle(song_locations) + self.random.shuffle(song_locations) fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") @@ -996,7 +998,7 @@ def prefill_state(base_state): 'Buy Goron Tunic': 1, 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement - self.multiworld.random.shuffle(shop_locations) + self.random.shuffle(shop_locations) self.pre_fill_items = [] # all prefill should be done fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, single_player_placement=True, lock=True, allow_excluded=True) @@ -1028,7 +1030,7 @@ def prefill_state(base_state): ganon_junk_fill = min(1, ganon_junk_fill) gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons)) locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None] - junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill)) + junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill)) exclusion_rules(self.multiworld, self.player, junk_fill_locations) # Locations which are not sendable must be converted to events @@ -1074,13 +1076,13 @@ def generate_output(self, output_directory: str): trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] self.trap_appearances = {} for loc_id in trap_location_ids: - self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name) + self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also - self.hint_rng = self.multiworld.per_slot_randoms[self.player] + self.hint_rng = self.random outfile_name = self.multiworld.get_out_file_name_base(self.player) - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) try: if self.hints != 'none': buildWorldGossipHints(self) @@ -1092,7 +1094,7 @@ def generate_output(self, output_directory: str): finally: self.collectible_flags_available.set() rom.update_header() - patch_data = create_patch_file(rom) + patch_data = create_patch_file(rom, self.random) rom.restore() apz5 = OoTContainer(patch_data, outfile_name, output_directory, @@ -1399,7 +1401,7 @@ def get_state_with_complete_itempool(self): return all_state def get_filler_item_name(self) -> str: - return get_junk_item(count=1, pool=get_junk_pool(self))[0] + return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0] def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool: From 926e08513c8b7fb2995b713d560fa165061e49a4 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 19 Sep 2024 01:57:59 +0200 Subject: [PATCH 288/393] The Witness: Remove some unused code #3852 --- worlds/witness/rules.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 2f3210a21467..74ea2aef5740 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -214,7 +214,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. """ - direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)] + direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] if not direct_items: return requirement_option @@ -224,7 +224,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S return [ rule for rule in requirement_option - if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]]) + if not (isinstance(rule, SimpleItemRepresentation) and rule[1] < max_per_item[rule[0]]) ] @@ -234,12 +234,6 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules. If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts() """ - converted_sublist = [] - - for rule in requirement: - if not isinstance(rule, tuple): - converted_sublist.append(rule) - continue collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] From 499d79f08954ca00e2b8b8876da01f52b24ca86f Mon Sep 17 00:00:00 2001 From: gaithern <36639398+gaithern@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:32:47 -0500 Subject: [PATCH 289/393] Kingdom Hearts: Fix Hint Spam and Add Setting Queries #3899 --- worlds/kh1/Client.py | 52 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/worlds/kh1/Client.py b/worlds/kh1/Client.py index acfd5dba3825..33fba85f6c54 100644 --- a/worlds/kh1/Client.py +++ b/worlds/kh1/Client.py @@ -31,6 +31,9 @@ def check_stdin() -> None: print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.") class KH1ClientCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx): + super().__init__(ctx) + def _cmd_deathlink(self): """Toggles Deathlink""" global death_link @@ -40,6 +43,40 @@ def _cmd_deathlink(self): else: death_link = True self.output(f"Death Link turned on") + + def _cmd_goal(self): + """Prints goal setting""" + if "goal" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["goal"])) + else: + self.output("Unknown") + + def _cmd_eotw_unlock(self): + """Prints End of the World Unlock setting""" + if "required_reports_door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["required_reports_door"] > 13: + self.output("Item") + else: + self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports") + else: + self.output("Unknown") + + def _cmd_door_unlock(self): + """Prints Final Rest Door Unlock setting""" + if "door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["door"] == "reports": + self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports") + else: + self.output(str(self.ctx.slot_data["door"])) + else: + self.output("Unknown") + + def _cmd_advanced_logic(self): + """Prints advanced logic setting""" + if "advanced_logic" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["advanced_logic"])) + else: + self.output("Unknown") class KH1Context(CommonContext): command_processor: int = KH1ClientCommandProcessor @@ -51,6 +88,8 @@ def __init__(self, server_address, password): self.send_index: int = 0 self.syncing = False self.awaiting_bridge = False + self.hinted_synth_location_ids = False + self.slot_data = {} # self.game_communication_path: files go in this path to pass data between us and the actual game if "localappdata" in os.environ: self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM") @@ -104,6 +143,7 @@ def on_package(self, cmd: str, args: dict): f.close() #Handle Slot Data + self.slot_data = args['slot_data'] for key in list(args['slot_data'].keys()): with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f: f.write(str(args['slot_data'][key])) @@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context): if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10: await ctx.send_death(death_text = "Sora was defeated!") if file.find("insynthshop") > -1: - await ctx.send_msgs([{ - "cmd": "LocationScouts", - "locations": [2656401,2656402,2656403,2656404,2656405,2656406], - "create_as_hint": 2 - }]) + if not ctx.hinted_synth_location_ids: + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": [2656401,2656402,2656403,2656404,2656405,2656406], + "create_as_hint": 2 + }]) + ctx.hinted_synth_location_ids = True ctx.locations_checked = sending message = [{"cmd": 'LocationChecks', "locations": sending}] await ctx.send_msgs(message) From 1b15c6920d88d333ba14dd33af75d42a59dfd826 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 20 Sep 2024 08:15:30 -0600 Subject: [PATCH 290/393] [OSRS] Adds display names to Options #3954 --- worlds/osrs/Options.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py index 520cd8e8b06b..81e017eddb34 100644 --- a/worlds/osrs/Options.py +++ b/worlds/osrs/Options.py @@ -63,6 +63,7 @@ class MaxCombatLevel(Range): The highest combat level of monster to possibly be assigned as a task. If set to 0, no combat tasks will be generated. """ + display_name = "Max Required Enemy Combat Level" range_start = 0 range_end = 1520 default = 50 @@ -74,6 +75,7 @@ class MaxCombatTasks(Range): If set to 0, no combat tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Combat Task Count" range_start = 0 range_end = MAX_COMBAT_TASKS default = MAX_COMBAT_TASKS @@ -85,6 +87,7 @@ class CombatTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Combat Task Weight" range_start = 0 range_end = 99 default = 50 @@ -95,6 +98,7 @@ class MaxPrayerLevel(Range): The highest Prayer requirement of any task generated. If set to 0, no Prayer tasks will be generated. """ + display_name = "Max Required Prayer Level" range_start = 0 range_end = 99 default = 50 @@ -106,6 +110,7 @@ class MaxPrayerTasks(Range): If set to 0, no Prayer tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Prayer Task Count" range_start = 0 range_end = MAX_PRAYER_TASKS default = MAX_PRAYER_TASKS @@ -117,6 +122,7 @@ class PrayerTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Prayer Task Weight" range_start = 0 range_end = 99 default = 50 @@ -127,6 +133,7 @@ class MaxMagicLevel(Range): The highest Magic requirement of any task generated. If set to 0, no Magic tasks will be generated. """ + display_name = "Max Required Magic Level" range_start = 0 range_end = 99 default = 50 @@ -138,6 +145,7 @@ class MaxMagicTasks(Range): If set to 0, no Magic tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Magic Task Count" range_start = 0 range_end = MAX_MAGIC_TASKS default = MAX_MAGIC_TASKS @@ -149,6 +157,7 @@ class MagicTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Magic Task Weight" range_start = 0 range_end = 99 default = 50 @@ -159,6 +168,7 @@ class MaxRunecraftLevel(Range): The highest Runecraft requirement of any task generated. If set to 0, no Runecraft tasks will be generated. """ + display_name = "Max Required Runecraft Level" range_start = 0 range_end = 99 default = 50 @@ -170,6 +180,7 @@ class MaxRunecraftTasks(Range): If set to 0, no Runecraft tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Runecraft Task Count" range_start = 0 range_end = MAX_RUNECRAFT_TASKS default = MAX_RUNECRAFT_TASKS @@ -181,6 +192,7 @@ class RunecraftTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Runecraft Task Weight" range_start = 0 range_end = 99 default = 50 @@ -191,6 +203,7 @@ class MaxCraftingLevel(Range): The highest Crafting requirement of any task generated. If set to 0, no Crafting tasks will be generated. """ + display_name = "Max Required Crafting Level" range_start = 0 range_end = 99 default = 50 @@ -202,6 +215,7 @@ class MaxCraftingTasks(Range): If set to 0, no Crafting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Crafting Task Count" range_start = 0 range_end = MAX_CRAFTING_TASKS default = MAX_CRAFTING_TASKS @@ -213,6 +227,7 @@ class CraftingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Crafting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -223,6 +238,7 @@ class MaxMiningLevel(Range): The highest Mining requirement of any task generated. If set to 0, no Mining tasks will be generated. """ + display_name = "Max Required Mining Level" range_start = 0 range_end = 99 default = 50 @@ -234,6 +250,7 @@ class MaxMiningTasks(Range): If set to 0, no Mining tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Mining Task Count" range_start = 0 range_end = MAX_MINING_TASKS default = MAX_MINING_TASKS @@ -245,6 +262,7 @@ class MiningTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Mining Task Weight" range_start = 0 range_end = 99 default = 50 @@ -255,6 +273,7 @@ class MaxSmithingLevel(Range): The highest Smithing requirement of any task generated. If set to 0, no Smithing tasks will be generated. """ + display_name = "Max Required Smithing Level" range_start = 0 range_end = 99 default = 50 @@ -266,6 +285,7 @@ class MaxSmithingTasks(Range): If set to 0, no Smithing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Smithing Task Count" range_start = 0 range_end = MAX_SMITHING_TASKS default = MAX_SMITHING_TASKS @@ -277,6 +297,7 @@ class SmithingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Smithing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -287,6 +308,7 @@ class MaxFishingLevel(Range): The highest Fishing requirement of any task generated. If set to 0, no Fishing tasks will be generated. """ + display_name = "Max Required Fishing Level" range_start = 0 range_end = 99 default = 50 @@ -298,6 +320,7 @@ class MaxFishingTasks(Range): If set to 0, no Fishing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Fishing Task Count" range_start = 0 range_end = MAX_FISHING_TASKS default = MAX_FISHING_TASKS @@ -309,6 +332,7 @@ class FishingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Fishing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -319,6 +343,7 @@ class MaxCookingLevel(Range): The highest Cooking requirement of any task generated. If set to 0, no Cooking tasks will be generated. """ + display_name = "Max Required Cooking Level" range_start = 0 range_end = 99 default = 50 @@ -330,6 +355,7 @@ class MaxCookingTasks(Range): If set to 0, no Cooking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Cooking Task Count" range_start = 0 range_end = MAX_COOKING_TASKS default = MAX_COOKING_TASKS @@ -341,6 +367,7 @@ class CookingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Cooking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -351,6 +378,7 @@ class MaxFiremakingLevel(Range): The highest Firemaking requirement of any task generated. If set to 0, no Firemaking tasks will be generated. """ + display_name = "Max Required Firemaking Level" range_start = 0 range_end = 99 default = 50 @@ -362,6 +390,7 @@ class MaxFiremakingTasks(Range): If set to 0, no Firemaking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Firemaking Task Count" range_start = 0 range_end = MAX_FIREMAKING_TASKS default = MAX_FIREMAKING_TASKS @@ -373,6 +402,7 @@ class FiremakingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Firemaking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -383,6 +413,7 @@ class MaxWoodcuttingLevel(Range): The highest Woodcutting requirement of any task generated. If set to 0, no Woodcutting tasks will be generated. """ + display_name = "Max Required Woodcutting Level" range_start = 0 range_end = 99 default = 50 @@ -394,6 +425,7 @@ class MaxWoodcuttingTasks(Range): If set to 0, no Woodcutting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Woodcutting Task Count" range_start = 0 range_end = MAX_WOODCUTTING_TASKS default = MAX_WOODCUTTING_TASKS @@ -405,6 +437,7 @@ class WoodcuttingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Woodcutting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -416,6 +449,7 @@ class MinimumGeneralTasks(Range): General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so there is no maximum. """ + display_name = "Minimum General Task Count" range_start = 0 range_end = NON_QUEST_LOCATION_COUNT default = 10 @@ -427,6 +461,7 @@ class GeneralTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "General Task Weight" range_start = 0 range_end = 99 default = 50 From 79942c09c2c082d2825af77d56369eb6fdc10b08 Mon Sep 17 00:00:00 2001 From: Alex Nordstrom Date: Fri, 20 Sep 2024 10:18:09 -0400 Subject: [PATCH 291/393] LADX: define filler item, fix for extra golden leaves (#3918) * set filler item also rename "Master Stalfos' Message" to "Nothing" as it shows up in game, and "Gel" to "Zol Attack" * fix for extra gold leaves * fix for start_inventory --- worlds/ladx/Items.py | 4 ++-- worlds/ladx/LADXR/patches/goldenLeaf.py | 3 ++- worlds/ladx/__init__.py | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index 9f4784f74995..1f9358a4f5a6 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -83,8 +83,8 @@ class ItemName: RUPEES_200 = "200 Rupees" RUPEES_500 = "500 Rupees" SEASHELL = "Seashell" - MESSAGE = "Master Stalfos' Message" - GEL = "Gel" + MESSAGE = "Nothing" + GEL = "Zol Attack" BOOMERANG = "Boomerang" HEART_PIECE = "Heart Piece" BOWWOW = "BowWow" diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py index 87cefae0f6d8..b35c722a4316 100644 --- a/worlds/ladx/LADXR/patches/goldenLeaf.py +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -29,6 +29,7 @@ def fixGoldenLeaf(rom): rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves - rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06")) + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason) # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 79f1fe470f81..2846b40e67d9 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -216,7 +216,7 @@ def create_items(self) -> None: for _ in range(count): if item_name in exclude: exclude.remove(item_name) # this is destructive. create unique list above - self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + self.multiworld.itempool.append(self.create_item("Nothing")) else: item = self.create_item(item_name) @@ -513,6 +513,9 @@ def remove(self, state, item: Item) -> bool: state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change + def get_filler_item_name(self) -> str: + return "Nothing" + def fill_slot_data(self): slot_data = {} From 0095eecf2b5c02f15e9121a238980cbd9e66ee3c Mon Sep 17 00:00:00 2001 From: Spineraks Date: Fri, 20 Sep 2024 19:07:45 +0200 Subject: [PATCH 292/393] Yacht Dice: Remove Victory item and make it an event instead (#3968) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name * Textual fixes and changes * Remove Victory item and use event instead. * Revert "Remove Victory item and use event instead." This reverts commit c2f7d674d392a3acbc1db8614411164ba3b28bff. * Revert "Textual fixes and changes" This reverts commit e9432f92454979fcd5a31f8517586585362a7ab7. * Remove Victory item and make it an event instead --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/Items.py | 2 -- worlds/yachtdice/__init__.py | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py index fa52c93ad6f2..c76dc538146e 100644 --- a/worlds/yachtdice/Items.py +++ b/worlds/yachtdice/Items.py @@ -16,8 +16,6 @@ class YachtDiceItem(Item): item_table = { - # victory item, always placed manually at goal location - "Victory": ItemData(16871244000 - 1, ItemClassification.progression), "Dice": ItemData(16871244000, ItemClassification.progression), "Dice Fragment": ItemData(16871244001, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression), diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index d86ee3382d33..75993fd39443 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -1,7 +1,7 @@ import math from typing import Dict -from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region, Tutorial from worlds.AutoWorld import WebWorld, World @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.2" + ap_world_version = "2.1.3" def _get_yachtdice_data(self): return { @@ -456,10 +456,12 @@ def create_regions(self): if loc_data.region == board.name ] - # Add the victory item to the correct location. - # The website declares that the game is complete when the victory item is obtained. + # Change the victory location to an event and place the Victory item there. victory_location_name = f"{self.goal_score} score" - self.get_location(victory_location_name).place_locked_item(self.create_item("Victory")) + self.get_location(victory_location_name).address = None + self.get_location(victory_location_name).place_locked_item( + Item("Victory", ItemClassification.progression, None, self.player) + ) # add the regions connection = Entrance(self.player, "New Board", menu) From ba8f03516e4d5453d9c148f89f0215611a4ef0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= <67028894+JoaoVictor-FA@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:19:48 -0300 Subject: [PATCH 293/393] Docs: added Brazilian Portuguese Translation for Hollow Knight setup guide (#3909) * add neww pt-br translation * setup file * Update setup_pt_br.md * add ` to paths * correct grammar * add space .-. * add more spaces .-. .-. .-. * capitalize HK * Update setup_pt_br.md * accent not the same as punctuation * small changes * Update setup_pt_br.md --- worlds/hk/__init__.py | 15 ++++++++-- worlds/hk/docs/setup_pt_br.md | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 worlds/hk/docs/setup_pt_br.md diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 860243ee952e..6ecdacb1156d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -124,14 +124,25 @@ class HKWeb(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Mod Setup and Use Guide", "A guide to playing Hollow Knight with Archipelago.", "English", "setup_en.md", "setup/en", ["Ijwu"] - )] + ) + + setup_pt_br = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Português Brasileiro", + "setup_pt_br.md", + "setup/pt_br", + ["JoaoVictor-FA"] + ) + + tutorials = [setup_en, setup_pt_br] bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" diff --git a/worlds/hk/docs/setup_pt_br.md b/worlds/hk/docs/setup_pt_br.md new file mode 100644 index 000000000000..9ae1ea89d566 --- /dev/null +++ b/worlds/hk/docs/setup_pt_br.md @@ -0,0 +1,52 @@ +# Guia de configuração para Hollow Knight no Archipelago + +## Programas obrigatórios +* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/). +* Uma cópia legal de Hollow Knight. + * Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas. + * Windows, Mac, e Linux (incluindo Steam Deck) são suportados. + +## Instalando o mod Archipelago Mod usando Lumafly +1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight. +2. Clique em "Install (instalar)" perto da opção "Archipelago" mod. + * Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo. +3. Abra o jogo, tudo preparado! + +### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação +1. Encontre a pasta manualmente. + * Xbox Game Pass: + 1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda. + 2. Clique nos 3 pontos depois clique gerenciar. + 3. Vá nos arquivos e selecione procurar. + 4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie. + * Steam: + 1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está. + . Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço. + * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight` + * Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight` + * Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app` +2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou. + +## Configurando seu arquivo YAML +### O que é um YAML e por que eu preciso de um? +Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago. +Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais. + +### Onde eu consigo o YAML? +Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago +para gerar o YAML usando a interface gráfica. + +### Entrando numa partida de Archipelago no Hollow Knight +1. Começe o jogo depois de instalar todos os mods necessários. +2. Crie um **novo jogo salvo.** +3. Selecione o modo de jogo **Archipelago** do menu de seleção. +4. Coloque as configurações corretas do seu servidor Archipelago. +5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens. +6. O jogo vai te colocar imediatamente numa partida randomizada. + * Se você está esperando uma contagem então espere ele cair antes de apertar começar. + * Ou clique em começar e pause o jogo enquanto estiver nele. + +## Dicas e outros comandos +Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no +[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso, +que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest). From 41ddb96b24cdec7886a4fa01cf42ef7e6e90e7bc Mon Sep 17 00:00:00 2001 From: qwint Date: Sat, 21 Sep 2024 09:45:22 -0500 Subject: [PATCH 294/393] HK: add race bool to slot data (#3971) --- worlds/hk/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 6ecdacb1156d..15addefef50a 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -21,6 +21,16 @@ from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld +from settings import Group, Bool + + +class HollowKnightSettings(Group): + class DisableMapModSpoilers(Bool): + """Disallows the APMapMod from showing spoiler placements.""" + + disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False + + path_of_pain_locations = { "Soul_Totem-Path_of_Pain_Below_Thornskip", "Lore_Tablet-Path_of_Pain_Entrance", @@ -156,6 +166,7 @@ class HKWorld(World): game: str = "Hollow Knight" options_dataclass = HKOptions options: HKOptions + settings: typing.ClassVar[HollowKnightSettings] web = HKWeb() @@ -555,6 +566,8 @@ def fill_slot_data(self): slot_data["grub_count"] = self.grub_count + slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race) + return slot_data def create_item(self, name: str) -> HKItem: From 69d3db21df580ba488f36a475d5ea98a34a3cf3b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 21 Sep 2024 17:02:58 -0400 Subject: [PATCH 295/393] TUNIC: Deal with the boxes blocking the entrance to Beneath the Vault --- worlds/tunic/er_rules.py | 5 ++++- worlds/tunic/rules.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index ee48f60eaca4..2677ec409b3b 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -762,7 +762,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world) - and has_lantern(state, world)) + and has_lantern(state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) + # on the reverse trip, you can lure an enemy over to break the boxes if needed 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, world)) diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 14ed84d44964..aa69666daeb6 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -114,7 +114,9 @@ def set_region_rules(world: "TunicWorld") -> None: or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ - lambda state: has_lantern(state, world) and has_ability(prayer, state, world) + lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) world.get_entrance("Ruined Atoll -> Library").access_rule = \ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) world.get_entrance("Overworld -> Quarry").access_rule = \ From 204e940f4741544eef50f12967cd177737d4023d Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 21 Sep 2024 17:05:00 -0400 Subject: [PATCH 296/393] Stardew Valley: Fix Art Of Crabbing Logic and Extract Festival Logic (#3625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * here you go kaito kid * here you go kaito kid * move reward logic in its own method --------- Co-authored-by: Jouramie Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> --- .../content/vanilla/pelican_town.py | 7 +- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/logic/festival_logic.py | 186 ++++++++++++++++++ worlds/stardew_valley/logic/logic.py | 141 +------------ 4 files changed, 192 insertions(+), 144 deletions(-) create mode 100644 worlds/stardew_valley/logic/festival_logic.py diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 73cc8f119a3e..913fe4b8ad96 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -2,7 +2,7 @@ from ...data import villagers_data, fish_data from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource -from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement +from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource from ...strings.book_names import Book from ...strings.crop_names import Fruit @@ -250,10 +250,7 @@ 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))), + CustomRuleSource(create_rule=lambda logic: logic.festival.has_squidfest_day_1_iridium_reward()), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.treasure_appraisal_guide: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index e026090f8659..64c14e9f678a 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -474,7 +474,7 @@ id,name,classification,groups,mod_name 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", +510,Book: The Art O' Crabbing,progression,"FESTIVAL", 511,Mr Qi's Plane Ride,progression,, 521,Power: Price Catalogue,useful,"BOOK_POWER", 522,Power: Mapping Cave Systems,useful,"BOOK_POWER", diff --git a/worlds/stardew_valley/logic/festival_logic.py b/worlds/stardew_valley/logic/festival_logic.py new file mode 100644 index 000000000000..2b22617202d8 --- /dev/null +++ b/worlds/stardew_valley/logic/festival_logic.py @@ -0,0 +1,186 @@ +from typing import Union + +from .action_logic import ActionLogicMixin +from .animal_logic import AnimalLogicMixin +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .fishing_logic import FishingLogicMixin +from .gift_logic import GiftLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .monster_logic import MonsterLogicMixin +from .museum_logic import MuseumLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .relationship_logic import RelationshipLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from ..options import FestivalLocations +from ..stardew_rule import StardewRule +from ..strings.book_names import Book +from ..strings.craftable_names import Fishing +from ..strings.crop_names import Fruit, Vegetable +from ..strings.festival_check_names import FestivalCheck +from ..strings.fish_names import Fish +from ..strings.forageable_names import Forageable +from ..strings.generic_names import Generic +from ..strings.machine_names import Machine +from ..strings.monster_names import Monster +from ..strings.region_names import Region + + +class FestivalLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.festival = FestivalLogic(*args, **kwargs) + + +class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin, +SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]): + + def initialize_rules(self): + self.registry.festival_rules.update({ + FestivalCheck.egg_hunt: self.logic.festival.can_win_egg_hunt(), + FestivalCheck.strawberry_seeds: self.logic.money.can_spend(1000), + FestivalCheck.dance: self.logic.relationship.has_hearts_with_any_bachelor(4), + FestivalCheck.tub_o_flowers: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_5: self.logic.money.can_spend(2500), + FestivalCheck.luau_soup: self.logic.festival.can_succeed_luau_soup(), + FestivalCheck.moonlight_jellies: self.logic.true_, + FestivalCheck.moonlight_jellies_banner: self.logic.money.can_spend(800), + FestivalCheck.starport_decal: self.logic.money.can_spend(1000), + FestivalCheck.smashing_stone: self.logic.true_, + FestivalCheck.grange_display: self.logic.festival.can_succeed_grange_display(), + FestivalCheck.rarecrow_1: self.logic.true_, # only cost star tokens + FestivalCheck.fair_stardrop: self.logic.true_, # only cost star tokens + FestivalCheck.spirit_eve_maze: self.logic.true_, + FestivalCheck.jack_o_lantern: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_2: self.logic.money.can_spend(5000), + FestivalCheck.fishing_competition: self.logic.festival.can_win_fishing_competition(), + FestivalCheck.rarecrow_4: self.logic.money.can_spend(5000), + FestivalCheck.mermaid_pearl: self.logic.has(Forageable.secret_note), + FestivalCheck.cone_hat: self.logic.money.can_spend(2500), + FestivalCheck.iridium_fireplace: self.logic.money.can_spend(15000), + FestivalCheck.rarecrow_7: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_artifacts(20), + FestivalCheck.rarecrow_8: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_items(40), + FestivalCheck.lupini_red_eagle: self.logic.money.can_spend(1200), + FestivalCheck.lupini_portrait_mermaid: self.logic.money.can_spend(1200), + FestivalCheck.lupini_solar_kingdom: self.logic.money.can_spend(1200), + FestivalCheck.lupini_clouds: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_1000_years: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_three_trees: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_the_serpent: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_tropical_fish: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_land_of_clay: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.secret_santa: self.logic.gifts.has_any_universal_love, + FestivalCheck.legend_of_the_winter_star: self.logic.true_, + FestivalCheck.rarecrow_3: self.logic.true_, + FestivalCheck.all_rarecrows: self.logic.region.can_reach(Region.farm) & self.logic.festival.has_all_rarecrows(), + FestivalCheck.calico_race: self.logic.true_, + FestivalCheck.mummy_mask: self.logic.true_, + FestivalCheck.calico_statue: self.logic.true_, + FestivalCheck.emily_outfit_service: self.logic.true_, + FestivalCheck.earthy_mousse: self.logic.true_, + FestivalCheck.sweet_bean_cake: self.logic.true_, + FestivalCheck.skull_cave_casserole: self.logic.true_, + FestivalCheck.spicy_tacos: self.logic.true_, + FestivalCheck.mountain_chili: self.logic.true_, + FestivalCheck.crystal_cake: self.logic.true_, + FestivalCheck.cave_kebab: self.logic.true_, + FestivalCheck.hot_log: self.logic.true_, + FestivalCheck.sour_salad: self.logic.true_, + FestivalCheck.superfood_cake: self.logic.true_, + FestivalCheck.warrior_smoothie: self.logic.true_, + FestivalCheck.rumpled_fruit_skin: self.logic.true_, + FestivalCheck.calico_pizza: self.logic.true_, + FestivalCheck.stuffed_mushrooms: self.logic.true_, + FestivalCheck.elf_quesadilla: self.logic.true_, + FestivalCheck.nachos_of_the_desert: self.logic.true_, + FestivalCheck.cloppino: self.logic.true_, + FestivalCheck.rainforest_shrimp: self.logic.true_, + FestivalCheck.shrimp_donut: self.logic.true_, + FestivalCheck.smell_of_the_sea: self.logic.true_, + FestivalCheck.desert_gumbo: self.logic.true_, + FestivalCheck.free_cactis: self.logic.true_, + FestivalCheck.monster_hunt: self.logic.monster.can_kill(Monster.serpent), + FestivalCheck.deep_dive: self.logic.region.can_reach(Region.skull_cavern_50), + FestivalCheck.treasure_hunt: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.touch_calico_statue: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.real_calico_egg_hunter: self.logic.region.can_reach(Region.skull_cavern_100), + FestivalCheck.willy_challenge: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.scorpion_carp]), + FestivalCheck.desert_scholar: self.logic.true_, + FestivalCheck.squidfest_day_1_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_1_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_1_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_1_iridium: self.logic.festival.can_squidfest_day_1_iridium_reward(), + FestivalCheck.squidfest_day_2_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_2_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_2_iridium: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & + self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]), + }) + for i in range(1, 11): + check_name = f"{FestivalCheck.trout_derby_reward_pattern}{i}" + self.registry.festival_rules[check_name] = self.logic.fishing.can_catch_fish(self.content.fishes[Fish.rainbow_trout]) + + def can_squidfest_day_1_iridium_reward(self) -> StardewRule: + return self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]) + + def has_squidfest_day_1_iridium_reward(self) -> StardewRule: + if self.options.festival_locations == FestivalLocations.option_disabled: + return self.logic.festival.can_squidfest_day_1_iridium_reward() + else: + return self.logic.received(f"Book: {Book.the_art_o_crabbing}") + + def can_win_egg_hunt(self) -> StardewRule: + return self.logic.true_ + + def can_succeed_luau_soup(self) -> StardewRule: + if self.options.festival_locations != FestivalLocations.option_hard: + return self.logic.true_ + 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.logic.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.logic.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] + aged_rule = self.logic.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 self.logic.true_ + + animal_rule = self.logic.animal.has_animal(Generic.any) + artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any) + cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough + fish_rule = self.logic.skill.can_fish(difficulty=50) + forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall + mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough + 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.logic.has_any(*good_fruits) + 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.logic.has_any(*good_vegetables) + + return animal_rule & artisan_rule & cooking_rule & fish_rule & \ + forage_rule & fruit_rule & mineral_rule & vegetable_rule + + def can_win_fishing_competition(self) -> StardewRule: + return self.logic.skill.can_fish(difficulty=60) + + def has_all_rarecrows(self) -> StardewRule: + rules = [] + for rarecrow_number in range(1, 9): + rules.append(self.logic.received(f"Rarecrow #{rarecrow_number}")) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index fb0d938fbb1e..9d4447439f7b 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -16,6 +16,7 @@ from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin from .farming_logic import FarmingLogicMixin +from .festival_logic import FestivalLogicMixin from .fishing_logic import FishingLogicMixin from .gift_logic import GiftLogicMixin from .grind_logic import GrindLogicMixin @@ -62,7 +63,6 @@ from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil -from ..strings.festival_check_names import FestivalCheck from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest from ..strings.flower_names import Flower from ..strings.food_names import Meal, Beverage @@ -94,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -363,89 +363,7 @@ def __init__(self, player: int, options: StardewValleyOptions, content: StardewC self.quest.initialize_rules() self.quest.update_rules(self.mod.quest.get_modded_quest_rules()) - 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_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(), - FestivalCheck.moonlight_jellies: True_(), - FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800), - FestivalCheck.starport_decal: self.money.can_spend(1000), - FestivalCheck.smashing_stone: True_(), - FestivalCheck.grange_display: self.can_succeed_grange_display(), - FestivalCheck.rarecrow_1: True_(), # only cost star tokens - FestivalCheck.fair_stardrop: True_(), # only cost star tokens - FestivalCheck.spirit_eve_maze: True_(), - FestivalCheck.jack_o_lantern: self.money.can_spend(2000), - FestivalCheck.rarecrow_2: self.money.can_spend(5000), - FestivalCheck.fishing_competition: self.can_win_fishing_competition(), - FestivalCheck.rarecrow_4: self.money.can_spend(5000), - FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note), - FestivalCheck.cone_hat: self.money.can_spend(2500), - FestivalCheck.iridium_fireplace: self.money.can_spend(15000), - FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20), - FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40), - FestivalCheck.lupini_red_eagle: self.money.can_spend(1200), - FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200), - FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200), - FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.secret_santa: self.gifts.has_any_universal_love, - 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.festival.initialize_rules() self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) @@ -486,53 +404,6 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: ] return self.count(12, *rules_worth_a_point) - def can_win_egg_hunt(self) -> StardewRule: - 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, 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 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 - 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 = (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 & \ - forage_rule & fruit_rule & mineral_rule & vegetable_rule - - def can_win_fishing_competition(self) -> StardewRule: - return self.skill.can_fish(difficulty=60) - def has_island_trader(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() @@ -571,12 +442,6 @@ def has_all_stardrops(self) -> StardewRule: 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 self.logic.and_(*rules) - def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) From 2b88be5791ae260048850ba652f2ba0aadeaeed9 Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Sat, 21 Sep 2024 14:06:31 -0700 Subject: [PATCH 297/393] Doom 1993 (auto-generated files): Update E4 logic (#3957) --- worlds/doom_1993/Locations.py | 4 ++-- worlds/doom_1993/Regions.py | 15 ++++++++++----- worlds/doom_1993/Rules.py | 5 ++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 2cbb9b9d150e..90a6916cd716 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -2214,13 +2214,13 @@ class LocationDict(TypedDict, total=False): 'map': 2, 'index': 217, 'doom_type': 2006, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351367: {'name': 'Perfect Hatred (E4M2) - Exit', 'episode': 4, 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 'episode': 4, 'map': 3, diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index f013bdceaf07..c32f7b470101 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -502,13 +502,12 @@ class RegionDict(TypedDict, total=False): "episode":4, "connections":[ {"target":"Perfect Hatred (E4M2) Blue","pro":False}, - {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}, + {"target":"Perfect Hatred (E4M2) Upper","pro":True}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, - "connections":[ - {"target":"Perfect Hatred (E4M2) Main","pro":False}, - {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, + "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, @@ -518,7 +517,13 @@ class RegionDict(TypedDict, total=False): {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, - "connections":[]}, + "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]}, + {"name":"Perfect Hatred (E4M2) Upper", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Perfect Hatred (E4M2) Cave","pro":False}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}]}, # Sever the Wicked (E4M3) {"name":"Sever the Wicked (E4M3) Main", diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 4faeb4a27dbd..89b09ff9f250 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro): state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("BFG9000", player, 1))) set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: - state.has("Shotgun", player, 1) or - state.has("Chaingun", player, 1) or - state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) + (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1))) # Perfect Hatred (E4M2) set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: From 97ca2ad258de7b4ea1f477ba409d36f5a15a0101 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:10:18 -0400 Subject: [PATCH 298/393] AHIT: Fix massive lag spikes in extremely large multiworlds, add extra security to prevent loading the wrong save file for a seed (#3718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec * typo Co-authored-by: Ixrec * Update worlds/ahit/Rules.py Co-authored-by: Ixrec * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * fix async lag * Update Client.py * shop item names need this now * fix indentation --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill --- worlds/ahit/Client.py | 38 ++++++++++++++++++++++++++++++++++++-- worlds/ahit/__init__.py | 3 ++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py index 2cd67e468294..cbb5f2a13d1f 100644 --- a/worlds/ahit/Client.py +++ b/worlds/ahit/Client.py @@ -4,7 +4,7 @@ import functools from copy import deepcopy from typing import List, Any, Iterable -from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer from MultiServer import Endpoint from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser @@ -101,12 +101,35 @@ def update_items(self): def on_package(self, cmd: str, args: dict): if cmd == "Connected": - self.connected_msg = encode([args]) + json = args + # This data is not needed and causes the game to freeze for long periods of time in large asyncs. + if "slot_info" in json.keys(): + json["slot_info"] = {} + if "players" in json.keys(): + me: NetworkPlayer + for n in json["players"]: + if n.slot == json["slot"] and n.team == json["team"]: + me = n + break + + # Only put our player info in there as we actually need it + json["players"] = [me] + if DEBUG: + print(json) + self.connected_msg = encode([json]) if self.awaiting_info: self.server_msgs.append(self.room_info) self.update_items() self.awaiting_info = False + elif cmd == "RoomUpdate": + # Same story as above + json = args + if "players" in json.keys(): + json["players"] = [] + + self.server_msgs.append(encode(json)) + elif cmd == "ReceivedItems": if args["index"] == 0: self.full_inventory.clear() @@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): await ctx.disconnect_proxy() break + if ctx.auth: + name = msg.get("name", "") + if name != "" and name != ctx.auth: + logger.info("Aborting proxy connection: player name mismatch from save file") + logger.info(f"Expected: {ctx.auth}, got: {name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - player name mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + if ctx.connected_msg and ctx.is_connected(): await ctx.send_msgs_proxy(ctx.connected_msg) ctx.update_items() diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index dd5e88abbc66..14cf13ec346d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -253,7 +253,8 @@ def fill_slot_data(self) -> dict: else: item_name = loc.item.name - shop_item_names.setdefault(str(loc.address), item_name) + shop_item_names.setdefault(str(loc.address), + f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})") slot_data["ShopItemNames"] = shop_item_names From 449782a4d89303ed03759a14e6b9ef92fc9ae07b Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 22 Sep 2024 10:21:10 -0400 Subject: [PATCH 299/393] TUNIC: Add forgotten Laurels rule for Beneath the Vault Boxes #3981 --- worlds/tunic/er_rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 2677ec409b3b..bd2498a56a35 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -764,7 +764,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) and has_lantern(state, world) # there's some boxes in the way - and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player))) # on the reverse trip, you can lure an enemy over to break the boxes if needed regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], From 99c02a3eb3157dfc345f770197372c59313d77d0 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sun, 22 Sep 2024 10:22:11 -0400 Subject: [PATCH 300/393] AHIT: Fix Death Wish option check typo (#3978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duh * Fuck it * Major fixes * a * b * Even more fixes * New option - NoFreeRoamFinale * a * Hat Logic Fix * Just to be safe * multiworld.random to world.random * KeyError fix * Update .gitignore * Update __init__.py * Zoinks Scoob * ffs * Ruh Roh Raggy, more r-r-r-random bugs! * 0.9b - cleanup + expanded logic difficulty * Update Rules.py * Update Regions.py * AttributeError fix * 0.10b - New Options * 1.0 Preparations * Docs * Docs 2 * Fixes * Update __init__.py * Fixes * variable capture my beloathed * Fixes * a * 10 Seconds logic fix * 1.1 * 1.2 * a * New client * More client changes * 1.3 * Final touch-ups for 1.3 * 1.3.1 * 1.3.3 * Zero Jumps gen error fix * more fixes * Formatting improvements * typo * Update __init__.py * Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. * init * Update to new options API * Missed some * Snatcher Coins fix * Missed some more * some slight touch ups * rewind * a * fix things * Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. * Update .gitignore * 1.3.6 * Final touch-ups * Fix client and leftover old options api * Delete setup-ahitclient.py * Update .gitignore * old python version fix * proper warnings for invalid act plandos * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update worlds/ahit/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * 120 char per line * "settings" to "options" * Update DeathWishRules.py * Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * No more loading the data package * cleanup + act plando fixes * almost forgot * Update Rules.py * a * Update worlds/ahit/Options.py Co-authored-by: Ixrec * Options stuff * oop * no unnecessary type hints * warn about depot download length in setup guide * Update worlds/ahit/Options.py Co-authored-by: Ixrec * typo Co-authored-by: Ixrec * Update worlds/ahit/Rules.py Co-authored-by: Ixrec * review stuff * More stuff from review * comment * 1.5 Update * link fix? * link fix 2 * Update setup_en.md * Update setup_en.md * Update setup_en.md * Evil * Good fucking lord * Review stuff again + Logic fixes * More review stuff * Even more review stuff - we're almost done * DW review stuff * Finish up review stuff * remove leftover stuff * a * assert item * add A Hat in Time to readme/codeowners files * Fix range options not being corrected properly * 120 chars per line in docs * Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * Remove some unnecessary option.class.value * Remove data_version and more option.class.value * Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener * Remove the rest of option.class.value * Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener * review stuff * Replace connect_regions with Region.connect * review stuff * Remove unnecessary Optional from LocData * Remove HatType.NONE * Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener * fix so default tests actually don't run * Improve performance for death wish rules * rename test file * change test imports * 1000 is probably unnecessary * a * change state.count to state.has * stuff * starting inventory hats fix * shouldn't have done this lol * make ship shape task goal equal to number of tasksanity checks if set to 0 * a * change act shuffle starting acts + logic updates * dumb * option groups + lambda capture cringe + typo * a * b * missing option in groups * c * Fix Your Contract Has Expired being placed on first level when it shouldn't * yche fix * formatting * major logic bug fix for death wish * Update Regions.py * Add missing indirect connections * Fix generation error from chapter 2 start with act shuffle off * a * Revert "a" This reverts commit df58bbcd998585760cc6ac9ea54b6fdf142b4fd1. * Revert "Fix generation error from chapter 2 start with act shuffle off" This reverts commit 0f4d441824af34bf7a7cff19f5f14161752d8661. * Fix option typo * I lied, it's actually two lines --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Ixrec Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill --- worlds/ahit/DeathWishLocations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index ef74cadcaa53..ce339c7c19bb 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"): for name in annoying_dws: world.excluded_dws.append(name) - if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: + if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses: for name in death_wishes: world.excluded_bonuses.append(name) - elif world.options.DWExcludeAnnoyingBonuses: + if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses: for name in annoying_bonuses: world.excluded_bonuses.append(name) From f7ec3d750873324fce6d671418d89ccb7439a5e4 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 22 Sep 2024 09:24:14 -0500 Subject: [PATCH 301/393] kvui: abstract away client tab additions #3950 --- WargrooveClient.py | 4 +--- kvui.py | 13 ++++++++++--- worlds/sc2/ClientGui.py | 5 +---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/WargrooveClient.py b/WargrooveClient.py index 39da044d659c..f9971f7a6c05 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -267,9 +267,7 @@ class WargrooveManager(GameManager): def build(self): container = super().build() - panel = TabbedPanelItem(text="Wargroove") - panel.content = self.build_tracker() - self.tabs.add_widget(panel) + self.add_client_tab("Wargroove", self.build_tracker()) return container def build_tracker(self) -> TrackerLayout: diff --git a/kvui.py b/kvui.py index 65cf52c7a4aa..536dce12208e 100644 --- a/kvui.py +++ b/kvui.py @@ -536,9 +536,8 @@ def connect_bar_validate(sender): # show Archipelago tab if other logging is present self.tabs.add_widget(panel) - hint_panel = TabbedPanelItem(text="Hints") - self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) - self.tabs.add_widget(hint_panel) + hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser)) + self.log_panels["Hints"] = hint_panel.content if len(self.logging_pairs) == 1: self.tabs.default_tab_text = "Archipelago" @@ -572,6 +571,14 @@ def connect_bar_validate(sender): return self.container + def add_client_tab(self, title: str, content: Widget) -> Widget: + """Adds a new tab to the client window with a given title, and provides a given Widget as its content. + Returns the new tab widget, with the provided content being placed on the tab as content.""" + new_tab = TabbedPanelItem(text=title) + new_tab.content = content + self.tabs.add_widget(new_tab) + return new_tab + def update_texts(self, dt): if hasattr(self.tabs.content.children[0], "fix_heights"): self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index fe62e6162457..51c55b437d92 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -111,13 +111,10 @@ def clear_tooltip(self) -> None: def build(self): container = super().build() - panel = TabbedPanelItem(text="Starcraft 2 Launcher") - panel.content = CampaignScroll() + panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) self.campaign_panel = MultiCampaignLayout() panel.content.add_widget(self.campaign_panel) - self.tabs.add_widget(panel) - Clock.schedule_interval(self.build_mission_table, 0.5) return container From d43dc6248506d3936a35063fa357352ad85f423b Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 22 Sep 2024 18:14:04 -0400 Subject: [PATCH 302/393] Stardew Valley: Improve Junimo Kart Regions #3984 --- worlds/stardew_valley/data/locations.csv | 4 ++-- worlds/stardew_valley/regions.py | 4 +++- worlds/stardew_valley/rules.py | 2 +- worlds/stardew_valley/strings/entrance_names.py | 1 + worlds/stardew_valley/strings/region_names.py | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 608b6a5f576a..680ddfcbacbf 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -313,14 +313,14 @@ id,region,name,tags,mod_name 611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK", 612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART", 613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART", -614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", +614,Junimo Kart 4,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", 615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART", 616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART", 617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART", 618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART", 619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART", 620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK", -621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", +621,Junimo Kart 4,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", 701,Secret Woods,Old Master Cannoli,MANDATORY, 702,Beach,Beach Bridge Repair,MANDATORY, 703,Desert,Galaxy Sword Shrine,MANDATORY, diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index b0fc7fa0ea52..5b7db5ac79d1 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -87,7 +87,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.jotpk_world_3), RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]), RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]), - RegionData(Region.junimo_kart_3), + RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]), + RegionData(Region.junimo_kart_4), RegionData(Region.alex_house), RegionData(Region.trailer), RegionData(Region.mayor_house), @@ -330,6 +331,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1), ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2), ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3), + ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4), ConnectionData(Entrance.town_to_sam_house, Region.sam_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.town_to_haley_house, Region.haley_house, diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7f39ee1ac2d4..eda2d4377e09 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -891,7 +891,7 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player logic.has("Junimo Kart Medium Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player), logic.has("Junimo Kart Big Buff")) - MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player), logic.has("Junimo Kart Max Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player), logic.has("JotPK Small Buff")) diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 58a919f2a8a4..b1c84004eb7a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -94,6 +94,7 @@ class Entrance: play_junimo_kart = "Play Junimo Kart" reach_junimo_kart_2 = "Reach Junimo Kart 2" reach_junimo_kart_3 = "Reach Junimo Kart 3" + reach_junimo_kart_4 = "Reach Junimo Kart 4" enter_locker_room = "Bathhouse Entrance to Locker Room" enter_public_bath = "Locker Room to Public Bath" enter_witch_swamp = "Witch Warp Cave to Witch's Swamp" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 58763b6fcb80..2bbc6228ab19 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -114,6 +114,7 @@ class Region: junimo_kart_1 = "Junimo Kart 1" junimo_kart_2 = "Junimo Kart 2" junimo_kart_3 = "Junimo Kart 3" + junimo_kart_4 = "Junimo Kart 4" mines_floor_5 = "The Mines - Floor 5" mines_floor_10 = "The Mines - Floor 10" mines_floor_15 = "The Mines - Floor 15" From 8021b457b6e0193b047f90de196963ee6460eaf1 Mon Sep 17 00:00:00 2001 From: Mrks <68022469+mrkssr@users.noreply.github.com> Date: Mon, 23 Sep 2024 23:19:26 +0200 Subject: [PATCH 303/393] WebHost: Added Games Of A Seed To The User Content Page (#3585) * Added contained games of a seed to the user content page as tooltip. * Changed sort handling. * Limited amount of shown games. * Added missing dashes. Co-authored-by: Kory Dondzila * Closing a-tags. Co-authored-by: Kory Dondzila * Closing a-tags. Co-authored-by: Kory Dondzila * Moved games list to table cell level. Co-authored-by: Kory Dondzila * Moved games list to table cell level. --------- Co-authored-by: Kory Dondzila --- WebHostLib/templates/userContent.html | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index 71a0f6747bc3..4e3747f4f952 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -1,5 +1,21 @@ {% extends 'tablepage.html' %} +{%- macro games(slots) -%} + {%- set gameList = [] -%} + {%- set maxGamesToShow = 10 -%} + + {%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%} + {% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%} + {% set _ = gameList.append(player) -%} + {%- endfor -%} + + {%- if slots|length > maxGamesToShow -%} + {% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%} + {%- endif -%} + + {{ gameList|join('\n') }} +{%- endmacro -%} + {% block head %} {{ super() }} User Content @@ -33,10 +49,12 @@

Your Rooms

{{ room.seed.id|suuid }} {{ room.id|suuid }} - {{ room.seed.slots|length }} + + {{ room.seed.slots|length }} + {{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} {{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} @@ -60,10 +78,15 @@

Your Seeds

{% for seed in seeds %} {{ seed.id|suuid }} - {% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %} + + {% if seed.multidata %} + {{ seed.slots|length }} + {% else %} + 1 + {% endif %} {{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} From f06d4503d83209b8fae6897eef500493d57826e8 Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Mon, 23 Sep 2024 16:21:03 -0500 Subject: [PATCH 304/393] Adds link to other players' trackers in player hints. (#3569) --- WebHostLib/templates/genericTracker.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 5a533204083b..947cf2837278 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -99,14 +99,18 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.finding_player)] }} + + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} From e910a372733aee02d37cd784ca2398874bea1a04 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:47:38 +0200 Subject: [PATCH 305/393] Core: Put an assert for parent region in Entrance.can_reach just like the one in Location.can_reach (#3998) * Core: Move connection.parent_region assert to can_reach This is how it already works for locations and it feels more correct to me to check in the place where the crash would happen. Also update location error to be a bit more verbose * Bring back the other assert * Update BaseClasses.py --- BaseClasses.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a5de1689a7fe..916a5b18042d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -720,7 +720,7 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu if new_region in reachable_regions: blocked_connections.remove(connection) elif connection.can_reach(self): - assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) blocked_connections.update(new_region.exits) @@ -946,6 +946,7 @@ def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) self.player = player def can_reach(self, state: CollectionState) -> bool: + assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region" if self.parent_region.can_reach(state) and self.access_rule(state): if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) @@ -1166,7 +1167,7 @@ def can_fill(self, state: CollectionState, item: Item, check_access: bool = True def can_reach(self, state: CollectionState) -> bool: # Region.can_reach is just a cache lookup, so placing it first for faster abort on average - assert self.parent_region, "Can't reach location without region" + assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region" return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): From 9a9fea0ca2d686ca350c93ae246e02da44a36b77 Mon Sep 17 00:00:00 2001 From: Felix R <50271878+FelicitusNeko@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:47:03 -0300 Subject: [PATCH 306/393] bumpstik: add hazard bumpers to completion (#3991) * bumpstik: add hazard bumpers to completion * bumpstik: update to use has_all_counts for completion as suggested by ScipioWright --- worlds/bumpstik/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index fe261dc94d30..ffe9efd2de87 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -125,6 +125,6 @@ def set_rules(self): lambda state: state.has("Hazard Bumper", self.player, 25) self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Booster Bumper", self.player, 5) and \ - state.has("Treasure Bumper", self.player, 32) + lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \ + self.player) From e85a835b47b082936b8fb7233d8857d6a0c81a17 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 26 Sep 2024 18:02:10 -0400 Subject: [PATCH 307/393] Core: use base collect/remove for item link groups (#3999) * use base collect/remove for item link groups * Update BaseClasses.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 916a5b18042d..0d4f34e51445 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -194,7 +194,9 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset( self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) - self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) + self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id]) + self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id]) + self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id]) self.player_name[new_id] = name new_group = self.groups[new_id] = Group(name=name, game=game, players=players, From a043ed50a6af54dd1b80efb06d251bc83e6ab2ad Mon Sep 17 00:00:00 2001 From: Benny D <78334662+benny-dreamly@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:56:36 -0600 Subject: [PATCH 308/393] Timespinner: Fix Typo in Download Location #3997 --- worlds/timespinner/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index f99dd7615571..2423e06bb010 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -207,7 +207,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), # 1337158 Is lost in time - LocationData('Library', 'Library: Terminal 3 (Emporer Nuvius)', 1337159, lambda state: state.has('Tablet', player)), + LocationData('Library', 'Library: Terminal 3 (Emperor Nuvius)', 1337159, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), From ab8caea8be1d8b38a1de8560c66cb66fb0e2873b Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Fri, 27 Sep 2024 00:57:21 +0200 Subject: [PATCH 309/393] SC2: Fix item origins, so including/excluding NCO/BW/EXT items works properly (#3990) --- worlds/sc2/Items.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 8277d0e7e13d..ee1f34d75be9 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -1274,16 +1274,16 @@ def get_full_item_list(): description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."), ItemNames.STRUCTURE_ARMOR: ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN, - description="Increases armor of all Terran structures by 2."), + description="Increases armor of all Terran structures by 2.", origin={"ext"}), ItemNames.HI_SEC_AUTO_TRACKING: ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN, - description="Increases attack range of all Terran structures by 1."), + description="Increases attack range of all Terran structures by 1.", origin={"ext"}), ItemNames.ADVANCED_OPTICS: ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN, - description="Increases attack range of all Terran mechanical units by 1."), + description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}), ItemNames.ROGUE_FORCES: ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN, - description="Mercenary calldowns are no longer limited by charges."), + description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}), ItemNames.ZEALOT: ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS, @@ -2369,7 +2369,8 @@ def get_basic_units(world: World, race: SC2Race) -> typing.Set[str]: ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, - ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL + ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, + ItemNames.PROGRESSIVE_ORBITAL_COMMAND } kerrigan_actives: typing.List[typing.Set[str]] = [ From 5ea55d77b0d2fbe5850c4b08665af64d75f75fa3 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 26 Sep 2024 18:25:41 -0500 Subject: [PATCH 310/393] The Messenger: add webhost auto connection steps to guide (#3904) * The Messenger: add webhost auto connection steps to guide and fix doc spacing * rever comments * add notes about potential steam popup * medic's feedback Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/messenger/docs/en_The Messenger.md | 18 ++++++++++-------- worlds/messenger/docs/setup_en.md | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 8248a4755d3f..a68ee5ba4c7a 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint -for it. The groups you can use for The Messenger are: +for it. + +The groups you can use for The Messenger are: * Notes - This covers the music notes * Keys - An alternative name for the music notes * Crest - The Sun and Moon Crests @@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are: * The player can return to the Tower of Time HQ at any point by selecting the button from the options menu * This can cause issues if used at specific times. If used in any of these known problematic areas, immediately -quit to title and reload the save. The currently known areas include: + quit to title and reload the save. The currently known areas include: * During Boss fights * After Courage Note collection (Corrupted Future chase) * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received * The mod option menu will also have a hint item button, as well as a release and collect button that are all placed -when the player fulfills the necessary conditions. + when the player fulfills the necessary conditions. * After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be -used to modify certain settings such as text size and color. This can also be used to specify a player name that can't -be entered in game. + used to modify certain settings such as text size and color. This can also be used to specify a player name that can't + be entered in game. ## Known issues * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit -to Searing Crags and re-enter to get it to play correctly. + to Searing Crags and re-enter to get it to play correctly. * Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left -and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock + and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock * Text entry menus don't accept controller input * In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the -chest will not work. + chest will not work. ## What do I do if I have a problem? diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index c1770e747442..64b706c2643a 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -41,14 +41,27 @@ These steps can also be followed to launch the game and check for mod updates af ## Joining a MultiWorld Game +### Automatic Connection on archipelago.gg + +1. Go to the room page of the MultiWorld you are going to join. +2. Click on your slot name on the left side. +3. Click the "The Messenger" button in the prompt. +4. Follow the remaining prompts. This process will check that you have the mod installed and will also check for updates + before launching The Messenger. If you are using the Steam version of The Messenger you may also get a prompt from + Steam asking if the game should be launched with arguments. These arguments are the URI which the mod uses to + connect. +5. Start a new save. You will already be connected in The Messenger and do not need to go through the menus. + +### Manual Connection + 1. Launch the game 2. Navigate to `Options > Archipelago Options` 3. Enter connection info using the relevant option buttons * **The game is limited to alphanumerical characters, `.`, and `-`.** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the -website. + website. * If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game -directory. When using this, all connection information must be entered in the file. + directory. When using this, all connection information must be entered in the file. 4. Select the `Connect to Archipelago` button 5. Navigate to save file selection 6. Start a new game From a2d585ba5cffd6e843e5355acb25a9be65c365b5 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 26 Sep 2024 19:26:06 -0400 Subject: [PATCH 311/393] Stardew Valley: Add Cinder Shard resource pack (#4001) * - Add Cinder Shard resource pack * - Make it ginger island exclusive --- worlds/stardew_valley/data/items.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 64c14e9f678a..ffcae223e251 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -819,6 +819,7 @@ id,name,classification,groups,mod_name 5289,Prismatic Shard,filler,"RESOURCE_PACK", 5290,Stardrop Tea,filler,"RESOURCE_PACK", 5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK", +5292,Resource Pack: 20 Cinder Shard,filler,"GINGER_ISLAND,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 From 5c4e81d04600ab4a2162bc19b11762ba055caaaa Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Thu, 26 Sep 2024 16:27:22 -0700 Subject: [PATCH 312/393] Hollow Knight: Clean outdated slot data code and comments #3988 --- worlds/hk/__init__.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 15addefef50a..9ec77e6bf0cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -534,26 +534,16 @@ def fill_slot_data(self): for option_name in hollow_knight_options: option = getattr(self.options, option_name) try: + # exclude more complex types - we only care about int, bool, enum for player options; the client + # can get them back to the necessary type. optionvalue = int(option.value) - except TypeError: - pass # C# side is currently typed as dict[str, int], drop what doesn't fit - else: options[option_name] = optionvalue + except TypeError: + pass # 32 bit int slot_data["seed"] = self.random.randint(-2147483647, 2147483646) - # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.options.CostSanity: - for shop, terms in shop_cost_types.items(): - unit = cost_terms[next(iter(terms))].option - if unit == "Geo": - continue - slot_data[f"{unit}_costs"] = { - loc.name: next(iter(loc.costs.values())) - for loc in self.created_multi_locations[shop] - } - # HKAP 0.1.0 and later cost data. location_costs = {} for region in self.multiworld.get_regions(self.player): @@ -566,7 +556,7 @@ def fill_slot_data(self): slot_data["grub_count"] = self.grub_count - slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race) + slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race return slot_data From 177c0fef52a7ebdde6778195ed5ed6acc1238207 Mon Sep 17 00:00:00 2001 From: soopercool101 Date: Thu, 26 Sep 2024 18:29:26 -0500 Subject: [PATCH 313/393] SM64: Remove outdated information on save bugs from setup guide (#3879) * Remove outdated information from SM64 setup guide Recent build changes have made it so that old saves no longer remove logical gates or prevent Toads from granting stars, remove info highlighting these issues. * Better line break location --- worlds/sm64ex/docs/setup_en.md | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 5983057f7d7a..7456bcb70b62 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -77,9 +77,6 @@ Should your name or password have spaces, enclose it in quotes: `"YourPassword"` Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. -**Important:** You must start a new file for every new seed you play. Using `â­x0` files is **not** sufficient. -Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file. - ### Playing offline To play offline, first generate a seed on the game's options page. @@ -129,18 +126,6 @@ To use this batch file, double-click it. A window will open. Type the five-digi Once you provide those two bits of information, the game will open. - If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. -### Addendum - Deleting old saves - -Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New". - -You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line: - -`del %AppData%\sm64ex\*.bin` - -`start sm64.us.f3dex2e.exe --sm64ap_file %1` - -This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one. - ## Installation Troubleshooting Start the game from the command line to view helpful messages regarding SM64EX. @@ -166,8 +151,9 @@ The Japanese Version should have no problem displaying these. ### Toad does not have an item for me. -This happens when you load an existing file that had already received an item from that toad. +This happens on older builds when you load an existing file that had already received an item from that toad. To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress. +Alternatively, updating your build will prevent this issue in the future. ### What happens if I lose connection? From 05439012dcd45cefd5ad99159024fb92d1213b8b Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Fri, 27 Sep 2024 01:30:23 +0200 Subject: [PATCH 314/393] Adjusts Whitespaces in the Plando Doc to be able to be copied directly (#3902) * Update plando_en.md * Also adjusts plando_connections indentation * ughh --- worlds/generic/docs/plando_en.md | 186 +++++++++++++++---------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 161b1e465b33..1980e81cbcc4 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -22,9 +22,9 @@ enabled (opt-in). * You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: ```yaml - requires: - version: current.version.number - plando: bosses, items, texts, connections +requires: + version: current.version.number + plando: bosses, items, texts, connections ``` ## Item Plando @@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap ### Examples ```yaml -plando_items: - # example block 1 - Timespinner - - item: - Empire Orb: 1 - Radiant Orb: 1 - location: Starter Chest 1 - from_pool: true - world: true - percentage: 50 - - # example block 2 - Ocarina of Time - - items: - Kokiri Sword: 1 - Biggoron Sword: 1 - Bow: 1 - Magic Meter: 1 - Progressive Strength Upgrade: 3 - Progressive Hookshot: 2 - locations: - - Deku Tree Slingshot Chest - - Dodongos Cavern Bomb Bag Chest - - Jabu Jabus Belly Boomerang Chest - - Bottom of the Well Lens of Truth Chest - - Forest Temple Bow Chest - - Fire Temple Megaton Hammer Chest - - Water Temple Longshot Chest - - Shadow Temple Hover Boots Chest - - Spirit Temple Silver Gauntlets Chest - world: false - - # example block 3 - Slay the Spire - - items: - Boss Relic: 3 - locations: - - Boss Relic 1 - - Boss Relic 2 - - Boss Relic 3 - - # example block 4 - Factorio - - items: - progressive-electric-energy-distribution: 2 - electric-energy-accumulators: 1 - progressive-turret: 2 - locations: - - military - - gun-turret - - logistic-science-pack - - steel-processing - percentage: 80 - force: true - -# example block 5 - Secret of Evermore - - items: - Levitate: 1 - Revealer: 1 - Energize: 1 - locations: - - Master Sword Pedestal - - Boss Relic 1 - world: true - count: 2 - -# example block 6 - A Link to the Past - - items: - Progressive Sword: 4 - world: - - BobsSlaytheSpire - - BobsRogueLegacy - count: - min: 1 - max: 4 + plando_items: + # example block 1 - Timespinner + - item: + Empire Orb: 1 + Radiant Orb: 1 + location: Starter Chest 1 + from_pool: true + world: true + percentage: 50 + + # example block 2 - Ocarina of Time + - items: + Kokiri Sword: 1 + Biggoron Sword: 1 + Bow: 1 + Magic Meter: 1 + Progressive Strength Upgrade: 3 + Progressive Hookshot: 2 + locations: + - Deku Tree Slingshot Chest + - Dodongos Cavern Bomb Bag Chest + - Jabu Jabus Belly Boomerang Chest + - Bottom of the Well Lens of Truth Chest + - Forest Temple Bow Chest + - Fire Temple Megaton Hammer Chest + - Water Temple Longshot Chest + - Shadow Temple Hover Boots Chest + - Spirit Temple Silver Gauntlets Chest + world: false + + # example block 3 - Slay the Spire + - items: + Boss Relic: 3 + locations: + - Boss Relic 1 + - Boss Relic 2 + - Boss Relic 3 + + # example block 4 - Factorio + - items: + progressive-electric-energy-distribution: 2 + electric-energy-accumulators: 1 + progressive-turret: 2 + locations: + - military + - gun-turret + - logistic-science-pack + - steel-processing + percentage: 80 + force: true + + # example block 5 - Secret of Evermore + - items: + Levitate: 1 + Revealer: 1 + Energize: 1 + locations: + - Master Sword Pedestal + - Boss Relic 1 + world: true + count: 2 + + # example block 6 - A Link to the Past + - items: + Progressive Sword: 4 + world: + - BobsSlaytheSpire + - BobsRogueLegacy + count: + min: 1 + max: 4 ``` 1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another player's Starter Chest 1 and removes the chosen item from the item pool. @@ -221,25 +221,25 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections). ### Examples ```yaml -plando_connections: - # example block 1 - A Link to the Past - - entrance: Cave Shop (Lake Hylia) - exit: Cave 45 - direction: entrance - - entrance: Cave 45 - exit: Cave Shop (Lake Hylia) - direction: entrance - - entrance: Agahnims Tower - exit: Old Man Cave Exit (West) - direction: exit - - # example block 2 - Minecraft - - entrance: Overworld Structure 1 - exit: Nether Fortress - direction: both - - entrance: Overworld Structure 2 - exit: Village - direction: both + plando_connections: + # example block 1 - A Link to the Past + - entrance: Cave Shop (Lake Hylia) + exit: Cave 45 + direction: entrance + - entrance: Cave 45 + exit: Cave Shop (Lake Hylia) + direction: entrance + - entrance: Agahnims Tower + exit: Old Man Cave Exit (West) + direction: exit + + # example block 2 - Minecraft + - entrance: Overworld Structure 1 + exit: Nether Fortress + direction: both + - entrance: Overworld Structure 2 + exit: Village + direction: both ``` 1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and From 3205e9b3a00763460af9481c78ac7124c19e09e0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 26 Sep 2024 23:31:50 +0000 Subject: [PATCH 315/393] DS3: Update setup instructions (#3817) * DS3: Point the DS3 client link to my GitHub It's not clear if/when my PR will land for the upstream fork, or if we'll just start using my fork as the primary source of truth. For now, it's the only one with 3.0.0-compatible releases. * DS3: Document Proton support * DS3: Document another way to get a YAML template * DS3: Don't say that the mod will force offline mode ModEngine2 is *supposed to* do this, but in practice it does not * Code review * Update Linux instructions per user experiences --- worlds/dark_souls_3/docs/setup_en.md | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index ed90289a8baf..9755cce1c6a8 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) -- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) +- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest) ## Optional Software @@ -11,8 +11,9 @@ ## Setting Up -First, download the client from the link above. It doesn't need to go into any particular directory; -it'll automatically locate _Dark Souls III_ in your Steam installation folder. +First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go +into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam +installation folder. Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This is the latest version, so you don't need to do any downpatching! However, if you've already @@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once To run _Dark Souls III_ in Archipelago mode: -1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the - DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. +1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain + scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu + screen. 2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that you can use to interact with the Archipelago server. @@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode: ### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to -configure your personal options and export them into a config file. +configure your personal options and export them into a config file. The [AP client archive] also +includes an options template. + +[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest + +### Does this work with Proton? + +The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few +things to keep in mind: + +* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install + the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under + plain WINE as well. It won't work as a Proton app! + +* To run the game itself, just run `launchmod_darksouls3.bat` under Proton. + +[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0 +[WINE]: https://www.winehq.org/ From 7337309426a247ff824b702389df6bfc87e381a6 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 26 Sep 2024 19:34:54 -0400 Subject: [PATCH 316/393] CommonClient: add more docstrings and comments #3821 --- CommonClient.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 6bdd8fc819da..1aedd518b4f8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -45,10 +45,21 @@ def get_ssl_context(): class ClientCommandProcessor(CommandProcessor): + """ + The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called + when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit". + + The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first + space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw + and method("one", "two", "three") without. + + In addition all docstrings for command methods will be displayed to the user on launch and when using "/help" + """ def __init__(self, ctx: CommonContext): self.ctx = ctx def output(self, text: str): + """Helper function to abstract logging to the CommonClient UI""" logger.info(text) def _cmd_exit(self) -> bool: @@ -164,13 +175,14 @@ def _cmd_ready(self): async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") def default(self, raw: str): + """The default message parser to be used when parsing any messages that do not match a command""" raw = self.ctx.on_user_say(raw) if raw: async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") class CommonContext: - # Should be adjusted as needed in subclasses + # The following attributes are used to Connect and should be adjusted as needed in subclasses tags: typing.Set[str] = {"AP"} game: typing.Optional[str] = None items_handling: typing.Optional[int] = None @@ -429,7 +441,10 @@ async def get_username(self): self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: - """ send `Connect` packet to log in to server """ + """ + Send a `Connect` packet to log in to the server, + additional keyword args can override any value in the connection packet + """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -459,6 +474,7 @@ def cancel_autoreconnect(self) -> bool: return False def slot_concerns_self(self, slot) -> bool: + """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly.""" if slot == self.slot: return True if slot in self.slot_info: @@ -466,6 +482,7 @@ def slot_concerns_self(self, slot) -> bool: return False def is_echoed_chat(self, print_json_packet: dict) -> bool: + """Helper function for filtering out messages sent by self.""" return print_json_packet.get("type", "") == "Chat" \ and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("slot", None) == self.slot @@ -497,13 +514,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]: """Gets called before sending a Say to the server from the user. Returned text is sent, or sending is aborted if None is returned.""" return text - + def on_ui_command(self, text: str) -> None: """Gets called by kivy when the user executes a command starting with `/` or `!`. The command processor is still called; this is just intended for command echoing.""" self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) def update_permissions(self, permissions: typing.Dict[str, int]): + """Internal method to parse and save server permissions from RoomInfo""" for permission_name, permission_flag in permissions.items(): try: flag = Permission(permission_flag) @@ -613,6 +631,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): + """Helper function to send a deathlink using death_text as the unique death cause string.""" if self.server and self.server.socket: logger.info("DeathLink: Sending death to your friends...") self.last_death_link = time.time() @@ -626,6 +645,7 @@ async def send_death(self, death_text: str = ""): }]) async def update_death_link(self, death_link: bool): + """Helper function to set Death Link connection tag on/off and update the connection if already connected.""" old_tags = self.tags.copy() if death_link: self.tags.add("DeathLink") @@ -635,7 +655,7 @@ async def update_death_link(self, death_link: bool): await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: - """Displays an error messagebox""" + """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework""" if not self.ui: return None title = title or "Error" @@ -987,6 +1007,7 @@ async def console_loop(ctx: CommonContext): def get_base_parser(description: typing.Optional[str] = None): + """Base argument parser to be reused for components subclassing off of CommonClient""" import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') @@ -1037,6 +1058,7 @@ async def main(args): parser.add_argument("url", nargs="?", help="Archipelago connection url") args = parser.parse_args(args) + # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost if args.url: url = urllib.parse.urlparse(args.url) if url.scheme == "archipelago": @@ -1048,6 +1070,7 @@ async def main(args): else: parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + # use colorama to display colored text highlighting on windows colorama.init() asyncio.run(main(args)) From de0c4984708cdfa7bea1f17d36a7ca15d34243d5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 28 Sep 2024 22:37:42 +0200 Subject: [PATCH 317/393] Core: update World method comment (#3866) --- worlds/AutoWorld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 19ec9a14a8c7..f7dae2b92750 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -342,7 +342,7 @@ def __getattr__(self, item: str) -> Any: # overridable methods that get called by Main.py, sorted by execution order # can also be implemented as a classmethod and called "stage_", - # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld. + # in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld. # An example of this can be found in alttp as stage_pre_fill @classmethod From 8193fa12b205f21bcfb1083961a6131962797dda Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 28 Sep 2024 13:49:11 -0700 Subject: [PATCH 318/393] BizHawkClient: Fix typing mistake (#3938) --- worlds/_bizhawk/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 74f2954b984b..3627f385c2d3 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -223,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None: raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") -async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: +async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]: """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected value. @@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[ return ret -async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: +async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]: """Reads data at 1 or more addresses. Items in `read_list` should be organized `(address, size, domain)` where @@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int return await guarded_read(ctx, read_list, []) -async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: +async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool: """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. Items in `write_list` should be organized `(address, value, domain)` where @@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl return True -async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: +async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None: """Writes data to 1 or more addresses. Items in write_list should be organized `(address, value, domain)` where From 67f6b458d7292c44a2a3870523d28868fbbb056c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 1 Oct 2024 14:08:13 -0500 Subject: [PATCH 319/393] Core: add race mode to multidata and datastore (#4017) * add race mode to multidata and datastore * have commonclient check race mode on connect and add it to the tooltip ui --- CommonClient.py | 2 ++ Main.py | 1 + MultiServer.py | 2 ++ kvui.py | 2 ++ 4 files changed, 7 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index 1aedd518b4f8..8325227d5e5c 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -325,6 +325,7 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing "collect": "disabled", "remaining": "disabled", } + self.race_mode: int = 0 # own state self.finished_game = False @@ -454,6 +455,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) + await self.send_msgs([{"cmd": "Get", "keys": ["race_mode"]}]) async def console_input(self) -> str: if self.ui: diff --git a/Main.py b/Main.py index 5a0f5c98bcc4..4008ca5e9017 100644 --- a/Main.py +++ b/Main.py @@ -338,6 +338,7 @@ def precollect_hint(location): "seed_name": multiworld.seed_name, "spheres": spheres, "datapackage": data_package, + "race_mode": int(multiworld.is_race), } AutoWorld.call_all(multiworld, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index e0b137fd68ce..91f4eec61574 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -427,6 +427,8 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A use_embedded_server_options: bool): self.read_data = {} + # there might be a better place to put this. + self.stored_data["race_mode"] = decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," diff --git a/kvui.py b/kvui.py index 536dce12208e..d58af3ed0e78 100644 --- a/kvui.py +++ b/kvui.py @@ -243,6 +243,8 @@ def get_text(self): f"\nYou currently have {ctx.hint_points} points." elif ctx.hint_cost == 0: text += "\n!hint is free to use." + if ctx.stored_data and "race_mode" in ctx.stored_data: + text += "\nRace mode is enabled." if ctx.stored_data["race_mode"] else "\nRace mode is disabled." else: text += f"\nYou are not authenticated yet." From dc1da4e88b4268ba12f6fd8b4a1ce362b4e9eebf Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 1 Oct 2024 12:08:43 -0700 Subject: [PATCH 320/393] Pokemon Emerald: Another wonder trade fix (#4014) * Pokemon Emerald: Another guarded write on wonder trades * Pokemon Emerald: Reorder sending wonder trade and erasing data In case the guarded write fails --- worlds/pokemon_emerald/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index c91b7d3e26b0..4405b34074e0 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -545,11 +545,12 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2: # Game has wonder trade data to send. Send it to data storage, remove it from the game's memory, # and mark that the game is waiting on receiving a trade - Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) - await bizhawk.write(ctx.bizhawk_ctx, [ + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ (sb1_address + 0x377C, bytes(0x50), "System Bus"), (sb1_address + 0x37CC, [1], "System Bus"), - ]) + ], [guards["SAVE BLOCK 1"]]) + if success: + Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2: # Game is waiting on receiving a trade. if self.queued_received_trade is not None: From 23469fa5c3113f83d21d608c25535aa1da95370b Mon Sep 17 00:00:00 2001 From: Alex Nordstrom Date: Tue, 1 Oct 2024 15:09:23 -0400 Subject: [PATCH 321/393] LADX: ghost fills ammo to initial max (#4005) * ghost fills ammo to max * Revert "ghost fills ammo to max" This reverts commit 68804fef1403197f2192e4c7d02f8793ac1c7ca0. * fill to first max --- worlds/ladx/LADXR/patches/owl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py index 47e575191a31..20b8221604c6 100644 --- a/worlds/ladx/LADXR/patches/owl.py +++ b/worlds/ladx/LADXR/patches/owl.py @@ -81,23 +81,23 @@ def removeOwlEvents(rom): ; Give powder ld a, [$DB4C] - cp $10 + cp $20 jr nc, doNotGivePowder - ld a, $10 + ld a, $20 ld [$DB4C], a doNotGivePowder: ld a, [$DB4D] - cp $10 + cp $30 jr nc, doNotGiveBombs - ld a, $10 + ld a, $30 ld [$DB4D], a doNotGiveBombs: ld a, [$DB45] - cp $10 + cp $30 jr nc, doNotGiveArrows - ld a, $10 + ld a, $30 ld [$DB45], a doNotGiveArrows: From 5a853dfccdfe138e00dff7dc55890980e71b7e05 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 1 Oct 2024 20:30:45 +0100 Subject: [PATCH 322/393] Tests: Fix indentation in TestTwoPlayerMulti (#4010) The "filling multiworld" subtest was at the wrong indentation, so was only running for the last world_type. "games" has additionally been added to the subtest to help better identify failures. Now that the subtest is actually being run for each world type, this adds about 20 seconds to the duration of the test on my machine. --- test/multiworld/test_multiworlds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 8415ac4c8429..3c1d0e4544eb 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -71,7 +71,7 @@ def test_two_player_single_game_fills(self) -> None: for world in self.multiworld.worlds.values(): world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) - with self.subTest("filling multiworld", seed=self.multiworld.seed): - distribute_items_restrictive(self.multiworld) - call_all(self.multiworld, "post_fill") - self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") + with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed): + distribute_items_restrictive(self.multiworld) + call_all(self.multiworld, "post_fill") + self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") From f06f95d03dd88f4a8aa35a88d2b75eedc7526a24 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 1 Oct 2024 16:55:34 -0500 Subject: [PATCH 323/393] Core: move race_mode to read_data instead of stored_data (#4020) * move race_mode to read_data * add race_mode to docs --- CommonClient.py | 3 +-- MultiServer.py | 2 +- docs/network protocol.md | 1 + kvui.py | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 8325227d5e5c..296c10ed4b4e 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -325,7 +325,6 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing "collect": "disabled", "remaining": "disabled", } - self.race_mode: int = 0 # own state self.finished_game = False @@ -455,7 +454,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) - await self.send_msgs([{"cmd": "Get", "keys": ["race_mode"]}]) + await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) async def console_input(self) -> str: if self.ui: diff --git a/MultiServer.py b/MultiServer.py index 91f4eec61574..c3e377e9a29d 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -428,7 +428,7 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A self.read_data = {} # there might be a better place to put this. - self.stored_data["race_mode"] = decoded_obj.get("race_mode", 0) + self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," diff --git a/docs/network protocol.md b/docs/network protocol.md index f8080fecc879..1c4579c4066f 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -395,6 +395,7 @@ Some special keys exist with specific return data, all of them have the prefix ` | item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | | location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | | client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | +| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. diff --git a/kvui.py b/kvui.py index d58af3ed0e78..74d8ad06734a 100644 --- a/kvui.py +++ b/kvui.py @@ -243,8 +243,9 @@ def get_text(self): f"\nYou currently have {ctx.hint_points} points." elif ctx.hint_cost == 0: text += "\n!hint is free to use." - if ctx.stored_data and "race_mode" in ctx.stored_data: - text += "\nRace mode is enabled." if ctx.stored_data["race_mode"] else "\nRace mode is disabled." + if ctx.stored_data and "_read_race_mode" in ctx.stored_data: + text += "\nRace mode is enabled." \ + if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled." else: text += f"\nYou are not authenticated yet." From 0ec9039ca6955129ba0fd15b0ba5a48cc71108da Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 2 Oct 2024 00:02:17 +0200 Subject: [PATCH 324/393] The Witness: Small code refactor (cast_not_none) (#3798) * cast not none * ruff * Missed a spot --- worlds/witness/__init__.py | 6 +++--- worlds/witness/data/utils.py | 7 ++++++- worlds/witness/player_items.py | 8 ++++---- worlds/witness/test/__init__.py | 5 +++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index b4b38c883e7d..c9848f2ffe47 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -14,7 +14,7 @@ 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 .data.utils import cast_not_none, get_audio_logs from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups @@ -55,7 +55,7 @@ class WitnessWorld(World): item_name_to_id = { # 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() + name: cast_not_none(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 @@ -336,7 +336,7 @@ def fill_slot_data(self) -> Dict[str, Any]: 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(cast(Location, hint.location)) + already_hinted_locations.add(cast_not_none(hint.location)) # Audio Log Hints diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 84eca5afc43f..737daff70fae 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,7 +1,7 @@ from math import floor from pkgutil import get_data from random import Random -from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar T = TypeVar("T") @@ -13,6 +13,11 @@ WitnessRule = FrozenSet[FrozenSet[str]] +def cast_not_none(value: Optional[T]) -> T: + assert value is not None + return value + + def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]: positions = range(len(population)) indices: List[int] = [] diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 72dfc2b7ee54..4c98cb78495e 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -15,7 +15,7 @@ ProgressiveItemDefinition, WeightedItemDefinition, ) -from .data.utils import build_weighted_int_list +from .data.utils import build_weighted_int_list, cast_not_none from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -200,7 +200,7 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ 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() + cast_not_none(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 ] @@ -211,8 +211,8 @@ def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: 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 options. - 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] + output[cast_not_none(item.ap_code)] = [cast_not_none(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/test/__init__.py b/worlds/witness/test/__init__.py index 4453609ddcdb..c3b427851af0 100644 --- a/worlds/witness/test/__init__.py +++ b/worlds/witness/test/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union from BaseClasses import CollectionState, Entrance, Item, Location, Region @@ -7,6 +7,7 @@ from test.multiworld.test_multiworlds import MultiworldTestBase from .. import WitnessWorld +from ..data.utils import cast_not_none class WitnessTestBase(WorldTestBase): @@ -32,7 +33,7 @@ def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance 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] + event_locations = [cast_not_none(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. From 05a67386c61c4be699b8a54067b84a72ea004126 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 2 Oct 2024 03:09:43 +0200 Subject: [PATCH 325/393] Core: use shlex splitting instead of whitespace splitting for client and server commands (#4011) --- MultiServer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index c3e377e9a29d..0fe950b5e4f3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -15,6 +15,7 @@ import operator import pickle import random +import shlex import threading import time import typing @@ -1152,7 +1153,7 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = raw.split() + command = shlex.split(raw, comments=False) basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) From 216e0603e1bcc4fea9985338212b333e38f1d468 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:27:23 -0500 Subject: [PATCH 326/393] KDL3: Fix webhost not giving a patch #4023 --- worlds/kdl3/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index f01c82dd16a3..1b5acbe97a3c 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -325,7 +325,7 @@ def generate_basic(self) -> None: def generate_output(self, output_directory: str) -> None: try: - patch = KDL3ProcedurePatch() + patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name) patch_rom(self, patch) self.rom_name = patch.name From e5a0ef799f513d3d6231140e5a4ba561b0bfdcd8 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Fri, 4 Oct 2024 12:28:43 -0700 Subject: [PATCH 327/393] Pokemon Emerald: Update changelog (#4003) --- worlds/pokemon_emerald/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 6a1844e79fde..2d7db0dad4d5 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -8,6 +8,9 @@ ### Fixes +- Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from +receiving new items. +- Fixed the client spamming the "goal complete" status update to the server instead of sending it once. - Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if the player randomized NPC gifts. - The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. From 97f2c25924b0b75f9dcb74e9dc28e5546a22b3e9 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:13:04 +0200 Subject: [PATCH 328/393] [KH2] Adds more options to slot data #4031 --- worlds/kh2/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index faf0bed88567..2809460aed6a 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -101,7 +101,18 @@ def fill_slot_data(self) -> dict: if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: self.goofy_ability_dict[ability] -= 1 - slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data = self.options.as_dict( + "Goal", + "FinalXemnas", + "LuckyEmblemsRequired", + "BountyRequired", + "FightLogic", + "FinalFormLogic", + "AutoFormLogic", + "LevelDepth", + "DonaldGoofyStatsanity", + "CorSkipToggle" + ) slot_data.update({ "hitlist": [], # remove this after next update "PoptrackerVersionCheck": 4.3, From 6287bc27a68ace32679fe8a41a580df7180cd9d8 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:14:22 +0200 Subject: [PATCH 329/393] WebHost: Fix too-many-players error not showing (#4033) * WebHost: fix 'too many players' error not showing * WebHost, Tests: add basic tests for generate endpoint * WebHost: hopefully make CodeQL happy with MAX_ROLL redirect --- WebHostLib/generate.py | 1 + test/webhost/test_generate.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/webhost/test_generate.py diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index dbe7dd958910..b19f3d483515 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]) elif len(gen_options) > app.config["MAX_ROLL"]: flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") + return redirect(url_for(request.endpoint, **(request.view_args or {}))) elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), diff --git a/test/webhost/test_generate.py b/test/webhost/test_generate.py new file mode 100644 index 000000000000..5440f6e02bec --- /dev/null +++ b/test/webhost/test_generate.py @@ -0,0 +1,73 @@ +import zipfile +from io import BytesIO + +from flask import url_for + +from . import TestBase + + +class TestGenerate(TestBase): + def test_valid_yaml(self) -> None: + """ + Verify that posting a valid yaml will start generating a game. + """ + with self.app.app_context(), self.app.test_request_context(): + yaml_data = """ + name: Player1 + game: Archipelago + Archipelago: {} + """ + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertTrue("/seed/" in response.request.path or + "/wait/" in response.request.path, + f"Response did not properly redirect ({response.request.path})") + + def test_empty_zip(self) -> None: + """ + Verify that posting an empty zip will give an error. + """ + with self.app.app_context(), self.app.test_request_context(): + zip_data = BytesIO() + zipfile.ZipFile(zip_data, "w").close() + zip_data.seek(0) + self.assertGreater(len(zip_data.read()), 0) + zip_data.seek(0) + response = self.client.post(url_for("generate"), + data={"file": (zip_data, "test.zip")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("not find any valid files", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") + + def test_too_many_players(self) -> None: + """ + Verify that posting too many players will give an error. + """ + max_roll = self.app.config["MAX_ROLL"] + # validate that max roll has a sensible value, otherwise we probably changed how it works + self.assertIsInstance(max_roll, int) + self.assertGreater(max_roll, 1) + self.assertLess(max_roll, 100) + # create a yaml with max_roll+1 players and watch it fail + with self.app.app_context(), self.app.test_request_context(): + yaml_data = "---\n".join([ + f"name: Player{n}\n" + "game: Archipelago\n" + "Archipelago: {}\n" + for n in range(1, max_roll + 2) + ]) + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("limited to", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") From 2751ccdaabc7e55fdfaa68cbb1d6ea9bb1d666ce Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:02:31 -0400 Subject: [PATCH 330/393] DS3: Make your own region cache (#4040) * Make your own region cache * Using a string --- worlds/dark_souls_3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b51668539be2..1aec6945eb8b 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -89,6 +89,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.all_excluded_locations = set() def generate_early(self) -> None: + self.created_regions = set() self.all_excluded_locations.update(self.options.exclude_locations.value) # Inform Universal Tracker where Yhorm is being randomized to. @@ -294,6 +295,7 @@ def create_region(self, region_name, location_table) -> Region: new_region.locations.append(new_location) self.multiworld.regions.append(new_region) + self.created_regions.add(region_name) return new_region def create_items(self) -> None: @@ -1305,7 +1307,7 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: """Sets a rule for the entrance to the given region.""" assert region in location_tables - if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if region not in self.created_regions: return if isinstance(rule, str): if " -> " not in rule: assert item_dictionary[rule].classification == ItemClassification.progression From f495bf726101e5125e73a1aabb23ee9ab6518d04 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 10 Oct 2024 20:05:21 -0500 Subject: [PATCH 331/393] The Messenger: fix missing money wrench rule (#4041) * The Messenger: fix missing money wrench rule * add a unit test for money wrench --- worlds/messenger/rules.py | 2 ++ worlds/messenger/test/test_shop.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 85b73dec4147..c354ad70aba6 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -220,6 +220,8 @@ def __init__(self, world: "MessengerWorld") -> None: } self.location_rules = { + # hq + "Money Wrench": self.can_shop, # ninja village "Ninja Village Seal - Tree House": self.has_dart, diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index 971ff1763b47..ce6fd19e33c8 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -1,5 +1,6 @@ from typing import Dict +from BaseClasses import CollectionState from . import MessengerTestBase from ..shop import SHOP_ITEMS, FIGURINES @@ -89,3 +90,15 @@ def test_costs(self) -> None: self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) + + max_cost_state = CollectionState(self.multiworld) + self.assertFalse(self.world.get_location("Money Wrench").can_reach(max_cost_state)) + prog_shards = [] + for item in self.multiworld.itempool: + if "Time Shard " in item.name: + value = int(item.name.strip("Time Shard ()")) + if value >= 100: + prog_shards.append(item) + for shard in prog_shards: + max_cost_state.collect(shard, True) + self.assertTrue(self.world.get_location("Money Wrench").can_reach(max_cost_state)) From ef4d1e77e3eec4ce0c7a4fe381b18487aedc8b40 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 11 Oct 2024 23:24:42 +0200 Subject: [PATCH 332/393] Core: make shlex split an attempt with regular split retry (#4046) --- MultiServer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 0fe950b5e4f3..8dfad5040de3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1153,7 +1153,10 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = shlex.split(raw, comments=False) + try: + command = shlex.split(raw, comments=False) + except ValueError: # most likely: "ValueError: No closing quotation" + command = raw.split() basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) From 2d0bdebaa9d58b5606dd60a773e45a3916658d71 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:01:28 -0500 Subject: [PATCH 333/393] Docs: Add ConnectUpdate to the list of client packets in the network protocol documentation #4045 --- docs/network protocol.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index 1c4579c4066f..4a96a43f818f 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -268,6 +268,7 @@ Additional arguments added to the [Set](#Set) package that triggered this [SetRe These packets are sent purely from client to server. They are not accepted by clients. * [Connect](#Connect) +* [ConnectUpdate](#ConnectUpdate) * [Sync](#Sync) * [LocationChecks](#LocationChecks) * [LocationScouts](#LocationScouts) From e8f3aa96dadef8651a05c0f070a1b8a361cb4eb9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:21:36 -0400 Subject: [PATCH 334/393] Timespinner: Two typos #4051 --- worlds/timespinner/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 66744cffdf85..f241d4468162 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -190,7 +190,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.has_replaced_options: warning = \ - f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ + f"NOTICE: Timespinner options for player '{self.player_name}' were renamed from PascalCase to snake_case, " \ "please update your yaml" spoiler_handle.write("\n") From b772d42df56d915927919bb6d4176576c19d95c0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 14 Oct 2024 00:15:53 +0200 Subject: [PATCH 335/393] Core: turn MultiServer item_names and location_names into instance vars (#4053) --- MultiServer.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8dfad5040de3..bac35648cf5a 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -185,11 +185,9 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) + item_names: typing.Dict[str, typing.Dict[int, str]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) + location_names: typing.Dict[str, typing.Dict[int, str]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -198,7 +196,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -269,6 +266,10 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.location_name_groups = {} self.all_item_and_group_names = {} self.all_location_and_group_names = {} + self.item_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')) + self.location_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() From d4d777b101f1759b0164181045cd7719d6b0d8ed Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 13 Oct 2024 18:17:53 -0400 Subject: [PATCH 336/393] OoT: Add aliases for Progressive Hookshot (#4052) * Add aliases for Progressive Hookshot * Update worlds/oot/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/oot/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b93f60b2a08e..c3925bf2a8bf 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -184,6 +184,10 @@ class OOTWorld(World): "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)"}, + + # aliases + "Longshot": {"Progressive Hookshot"}, # fuzzy hinting thought Longshot was Slingshot + "Hookshot": {"Progressive Hookshot"}, # for consistency, mostly } location_name_groups = build_location_name_groups() From f2ac937d1e6d2964ab1f5951cdb54f47bdd96e78 Mon Sep 17 00:00:00 2001 From: Seafo <92278897+Seatori@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:22:37 -0400 Subject: [PATCH 337/393] Minecraft: Fix plando connections #4048 Plando connections was broken as a result of https://github.com/ArchipelagoMW/Archipelago/pull/3765 This fixes it. --- worlds/minecraft/Structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py index df3d944a6c65..d4f62f3498e9 100644 --- a/worlds/minecraft/Structures.py +++ b/worlds/minecraft/Structures.py @@ -29,7 +29,7 @@ def set_pair(exit, struct): # Connect plando structures first if self.options.plando_connections: - for conn in self.plando_connections: + for conn in self.options.plando_connections: set_pair(conn.entrance, conn.exit) # The algorithm tries to place the most restrictive structures first. This algorithm always works on the From 618564c60a1ae18d26d09688e08fc7df9e61dd8c Mon Sep 17 00:00:00 2001 From: Louis M Date: Mon, 14 Oct 2024 12:53:20 -0400 Subject: [PATCH 338/393] Aquaria: Adding slot data for poptracker (#4056) * Adds neccessary slot data for Aquaria * Comma oops --------- Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com> --- worlds/aquaria/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 1fb04036d81b..dd17d09d8a6a 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -212,4 +212,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "skip_first_vision": bool(self.options.skip_first_vision.value), "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3], "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3], + "bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb), + "no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations), + "light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places), + "turtle_randomizer": self.options.turtle_randomizer.value, } From af0b5f8cf2c1506c6bae4ccd0d113960a768a860 Mon Sep 17 00:00:00 2001 From: Louis M Date: Tue, 15 Oct 2024 17:22:58 -0400 Subject: [PATCH 339/393] Aquaria Fixing some bugs (#4057) * Fixing some bugs * Forgot about this one --- worlds/aquaria/Regions.py | 21 +++++++++------------ worlds/aquaria/__init__.py | 8 +++++--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 3ec1fb880e13..792d7b73dfdb 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -738,9 +738,7 @@ def __connect_veil_regions(self) -> None: self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", - self.sun_temple_l, self.sun_temple_boss_path, - lambda state: _has_light(state, self.player) or - _has_sun_crystal(state, self.player)) + self.sun_temple_l, self.sun_temple_boss_path) self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_attack_item(state, self.player)) @@ -775,14 +773,11 @@ def __connect_abyss_regions(self) -> None: self.abyss_l, self.king_jellyfish_cave, lambda state: (_has_energy_form(state, self.player) and _has_beast_form(state, self.player)) or - _has_dual_form(state, self.player)) + _has_dual_form(state, self.player)) self.__connect_regions("Abyss left area", "Abyss right area", self.abyss_l, self.abyss_r) - self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle", + self.__connect_regions("Abyss right area", "Abyss right area, transturtle", self.abyss_r, self.abyss_r_transturtle) - self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area", - self.abyss_r_transturtle, self.abyss_r, - lambda state: _has_light(state, self.player)) self.__connect_regions("Abyss right area", "Inside the whale", self.abyss_r, self.whale, lambda state: _has_spirit_form(state, self.player) and @@ -1092,12 +1087,10 @@ def __adjusting_light_in_dark_place_rules(self) -> None: lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player), - lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player), - lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) + add_rule(self.multiworld.get_entrance("Abyss right area, transturtle to Abyss right area", self.player), + lambda state: _has_light(state, self.player)) def __adjusting_manual_rules(self) -> None: add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), @@ -1151,6 +1144,10 @@ def __adjusting_manual_rules(self) -> None: lambda state: state.has("Sun God beated", self.player)) add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), lambda state: _has_tongue_cleared(state, self.player)) + add_rule(self.multiworld.get_location( + "Open Water top right area, bulb in the small path before Mithalas", + self.player), lambda state: _has_bind_song(state, self.player) + ) def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index dd17d09d8a6a..f79978f25fc4 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -130,12 +130,13 @@ def create_item(self, name: str) -> AquariaItem: return result - def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None: + def __pre_fill_item(self, item_name: str, location_name: str, precollected, + itemClassification: ItemClassification = ItemClassification.useful) -> None: """Pre-assign an item to a location""" if item_name not in precollected: self.exclude.append(item_name) data = item_table[item_name] - item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player) + item = AquariaItem(item_name, itemClassification, data.id, self.player) self.multiworld.get_location(location_name, self.player).place_locked_item(item) def get_filler_item_name(self): @@ -164,7 +165,8 @@ def create_items(self) -> None: self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) # The last two are inverted because in the original game, they are special turtle that communicate directly - self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) + self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected, + ItemClassification.progression) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): if name not in self.exclude: From 26577b16dce3a69efc2bd68d02403922206bd002 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 15 Oct 2024 14:28:36 -0700 Subject: [PATCH 340/393] Pokemon Emerald: Fix opponent blacklist checking wrong option (#4058) --- worlds/pokemon_emerald/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index d281dde23cb0..a87f93ece56b 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -177,7 +177,7 @@ def generate_early(self) -> None: for species_name in self.options.trainer_party_blacklist.value if species_name != "_Legendaries" } - if "_Legendaries" in self.options.starter_blacklist.value: + if "_Legendaries" in self.options.trainer_party_blacklist.value: self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON # In race mode we don't patch any item location information into the ROM From c12ed316cff03014810aa610248f95cbb53036af Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 16 Oct 2024 23:06:14 +0200 Subject: [PATCH 341/393] Timespinner: Make hidden options pickleables (#4050) * Make timespinner hidden options pickleables * Keep changes minimal * Change line endings --- worlds/timespinner/Options.py | 81 ++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 20ad8132c45f..f6a3dba3e311 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -417,13 +417,16 @@ class HiddenTraps(Traps): """List of traps that may be in the item pool to find""" visibility = Visibility.none -class OptionsHider: - @classmethod - def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: - new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) - new_option.visibility = Visibility.none - new_option.__doc__ = option.__doc__ - return new_option +class HiddenDeathLink(DeathLink): + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" + visibility = Visibility.none + +def hidden(option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option.__name__}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ + globals()[f"{option.__name__}Hidden"] = new_option + return new_option class HasReplacedCamelCase(Toggle): """For internal use will display a warning message if true""" @@ -431,41 +434,41 @@ class HasReplacedCamelCase(Toggle): @dataclass class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): - StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore - DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore - EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore - StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore - QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore - SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore - Inverted: OptionsHider.hidden(Inverted) # type: ignore - GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore - Cantoran: OptionsHider.hidden(Cantoran) # type: ignore - LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore - BossRando: OptionsHider.hidden(BossRando) # type: ignore - DamageRando: OptionsHider.hidden(DamageRando) # type: ignore + StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: hidden(DownloadableItems) # type: ignore + EyeSpy: hidden(EyeSpy) # type: ignore + StartWithMeyef: hidden(StartWithMeyef) # type: ignore + QuickSeed: hidden(QuickSeed) # type: ignore + SpecificKeycards: hidden(SpecificKeycards) # type: ignore + Inverted: hidden(Inverted) # type: ignore + GyreArchives: hidden(GyreArchives) # type: ignore + Cantoran: hidden(Cantoran) # type: ignore + LoreChecks: hidden(LoreChecks) # type: ignore + BossRando: hidden(BossRando) # type: ignore + DamageRando: hidden(DamageRando) # type: ignore DamageRandoOverrides: HiddenDamageRandoOverrides - HpCap: OptionsHider.hidden(HpCap) # type: ignore - LevelCap: OptionsHider.hidden(LevelCap) # type: ignore - ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore - BossHealing: OptionsHider.hidden(BossHealing) # type: ignore - ShopFill: OptionsHider.hidden(ShopFill) # type: ignore - ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore - ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore - LootPool: OptionsHider.hidden(LootPool) # type: ignore - DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore - FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore - LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore - ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore - ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore - EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore - DadPercent: OptionsHider.hidden(DadPercent) # type: ignore - RisingTides: OptionsHider.hidden(RisingTides) # type: ignore + HpCap: hidden(HpCap) # type: ignore + LevelCap: hidden(LevelCap) # type: ignore + ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore + BossHealing: hidden(BossHealing) # type: ignore + ShopFill: hidden(ShopFill) # type: ignore + ShopWarpShards: hidden(ShopWarpShards) # type: ignore + ShopMultiplier: hidden(ShopMultiplier) # type: ignore + LootPool: hidden(LootPool) # type: ignore + DropRateCategory: hidden(DropRateCategory) # type: ignore + FixedDropRate: hidden(FixedDropRate) # type: ignore + LootTierDistro: hidden(LootTierDistro) # type: ignore + ShowBestiary: hidden(ShowBestiary) # type: ignore + ShowDrops: hidden(ShowDrops) # type: ignore + EnterSandman: hidden(EnterSandman) # type: ignore + DadPercent: hidden(DadPercent) # type: ignore + RisingTides: hidden(RisingTides) # type: ignore RisingTidesOverrides: HiddenRisingTidesOverrides - UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore - PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore - TrapChance: OptionsHider.hidden(TrapChance) # type: ignore + UnchainedKeys: hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: hidden(TrapChance) # type: ignore Traps: HiddenTraps # type: ignore - DeathLink: OptionsHider.hidden(DeathLink) # type: ignore + DeathLink: HiddenDeathLink # type: ignore has_replaced_options: HasReplacedCamelCase def handle_backward_compatibility(self) -> None: From 375b5796d95774f152d5f85cb97dd3af8f307a59 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 16 Oct 2024 23:28:42 +0200 Subject: [PATCH 342/393] WebHost: noscript faq and glossary (#4061) --- WebHostLib/misc.py | 23 +++++++-- WebHostLib/requirements.txt | 2 + WebHostLib/static/assets/faq.js | 51 ------------------- .../static/assets/faq/{faq_en.md => en.md} | 0 WebHostLib/static/assets/glossary.js | 51 ------------------- .../{faq/glossary_en.md => glossary/en.md} | 0 WebHostLib/templates/faq.html | 17 ------- WebHostLib/templates/glossary.html | 17 ------- WebHostLib/templates/markdown_document.html | 13 +++++ 9 files changed, 34 insertions(+), 140 deletions(-) delete mode 100644 WebHostLib/static/assets/faq.js rename WebHostLib/static/assets/faq/{faq_en.md => en.md} (100%) delete mode 100644 WebHostLib/static/assets/glossary.js rename WebHostLib/static/assets/{faq/glossary_en.md => glossary/en.md} (100%) delete mode 100644 WebHostLib/templates/faq.html delete mode 100644 WebHostLib/templates/glossary.html create mode 100644 WebHostLib/templates/markdown_document.html diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 4784fcd9da63..1f86e21066ef 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -5,6 +5,7 @@ import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from pony.orm import count, commit, db_session +from werkzeug.utils import secure_filename from worlds.AutoWorld import AutoWorldRegister from . import app, cache @@ -69,14 +70,28 @@ def tutorial_landing(): @app.route('/faq//') @cache.cached() -def faq(lang): - return render_template("faq.html", lang=lang) +def faq(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Frequently Asked Questions", + html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]), + ) @app.route('/glossary//') @cache.cached() -def terms(lang): - return render_template("glossary.html", lang=lang) +def glossary(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Glossary", + html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]), + ) @app.route('/seed/') diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c593cd63df7e..2020387053f9 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -9,3 +9,5 @@ bokeh>=3.1.1; python_version <= '3.8' bokeh>=3.4.3; python_version == '3.9' bokeh>=3.5.2; python_version >= '3.10' markupsafe>=2.1.5 +Markdown>=3.7 +mdx-breakless-lists>=1.0.1 diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js deleted file mode 100644 index 1bf5e5a65995..000000000000 --- a/WebHostLib/static/assets/faq.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('faq-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the tutorial is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the tutorial."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/en.md similarity index 100% rename from WebHostLib/static/assets/faq/faq_en.md rename to WebHostLib/static/assets/faq/en.md diff --git a/WebHostLib/static/assets/glossary.js b/WebHostLib/static/assets/glossary.js deleted file mode 100644 index 04a292008655..000000000000 --- a/WebHostLib/static/assets/glossary.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('glossary-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the glossary page is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the glossary."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/glossary_en.md b/WebHostLib/static/assets/glossary/en.md similarity index 100% rename from WebHostLib/static/assets/faq/glossary_en.md rename to WebHostLib/static/assets/glossary/en.md diff --git a/WebHostLib/templates/faq.html b/WebHostLib/templates/faq.html deleted file mode 100644 index 76bdb96d2ef8..000000000000 --- a/WebHostLib/templates/faq.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Frequently Asked Questions - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/glossary.html b/WebHostLib/templates/glossary.html deleted file mode 100644 index 921f678157fc..000000000000 --- a/WebHostLib/templates/glossary.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Glossary - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/markdown_document.html b/WebHostLib/templates/markdown_document.html new file mode 100644 index 000000000000..07b3c8354d0d --- /dev/null +++ b/WebHostLib/templates/markdown_document.html @@ -0,0 +1,13 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {% include 'header/grassHeader.html' %} + {{ title }} + +{% endblock %} + +{% block body %} +
+ {{ html_from_markdown | safe}} +
+{% endblock %} From 48822227b5e082d7aa9aaf1f06cdc6504113a509 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 16 Oct 2024 23:31:36 +0200 Subject: [PATCH 343/393] Test: option instances have to be pickleable (#4006) --- test/general/test_options.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/general/test_options.py b/test/general/test_options.py index 2229b7ea7e66..ee2f22a6dc71 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -59,3 +59,12 @@ def test_item_links_resolve(self): item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} for link in item_links.values(): self.assertEqual(link.value[0], item_link_group[0]) + + def test_pickle_dumps(self): + """Test options can be pickled into database for WebHost generation""" + import pickle + for gamename, world_type in AutoWorldRegister.world_types.items(): + if not world_type.hidden: + for option_key, option in world_type.options_dataclass.type_hints.items(): + with self.subTest(game=gamename, option=option_key): + pickle.dumps(option(option.default)) From 2b0cab82fa7779a1ec2ed0f114437c9efc3acd22 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Thu, 17 Oct 2024 00:14:27 +0200 Subject: [PATCH 344/393] CommonClient: Making local datapackage load correctly if it was overriden by a custom one (#3722) * Added versions and checksums dict * Added load of local datapackage * Fixed typo --- CommonClient.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 296c10ed4b4e..77ed85b5c652 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -355,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing self.item_names = self.NameLookupDict(self, "item") self.location_names = self.NameLookupDict(self, "location") + self.versions = {} + self.checksums = {} self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) @@ -571,26 +573,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], needed_updates.add(game) continue - local_version: int = network_data_package["games"].get(game, {}).get("version", 0) - local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") - # no action required if local version is new enough - if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ - or remote_checksum != local_checksum: - cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) - cache_version: int = cached_game.get("version", 0) - cache_checksum: typing.Optional[str] = cached_game.get("checksum") - # download remote version if cache is not new enough - if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ - or remote_checksum != cache_checksum: - needed_updates.add(game) + cached_version: int = self.versions.get(game, 0) + cached_checksum: typing.Optional[str] = self.checksums.get(game) + # no action required if cached version is new enough + if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \ + or remote_checksum != cached_checksum: + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") + if ((remote_checksum or remote_version <= local_version and remote_version != 0) + and remote_checksum == local_checksum): + self.update_game(network_data_package["games"][game], game) else: - self.update_game(cached_game, game) + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") + # download remote version if cache is not new enough + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: + needed_updates.add(game) + else: + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) def update_game(self, game_package: dict, game: str): self.item_names.update_game(game, game_package["item_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"]) + self.versions[game] = game_package.get("version", 0) + self.checksums[game] = game_package.get("checksum") def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): From 79cec89e242e370bf2756d5a4ead971965939aa6 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 16 Oct 2024 18:27:50 -0400 Subject: [PATCH 345/393] Launcher: save default settings before opening file for users (#4042) --- Launcher.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Launcher.py b/Launcher.py index 42f93547cc9d..85e49da7e551 100644 --- a/Launcher.py +++ b/Launcher.py @@ -35,7 +35,9 @@ def open_host_yaml(): - file = settings.get_settings().filename + s = settings.get_settings() + file = s.filename + s.save() assert file, "host.yaml missing" if is_linux: exe = which('sensible-editor') or which('gedit') or \ From a0f49dd7d95d352d8da2b4a8b03335be7f77d5b3 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 16 Oct 2024 21:31:53 -0400 Subject: [PATCH 346/393] Noita: Add the useful classification to important perks, making them progression + useful #4030 --- worlds/noita/items.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/noita/items.py b/worlds/noita/items.py index 6b662fbee692..1cb7d9601386 100644 --- a/worlds/noita/items.py +++ b/worlds/noita/items.py @@ -100,13 +100,13 @@ def create_all_items(world: NoitaWorld) -> None: "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), - "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), - "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), - "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1), - "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1), - "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1), - "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), - "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), + "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), From ff297f29517f52b29335a00f740a74b50c7d6662 Mon Sep 17 00:00:00 2001 From: palex00 <32203971+palex00@users.noreply.github.com> Date: Thu, 17 Oct 2024 03:34:10 +0200 Subject: [PATCH 347/393] [Aquaria] Adds Poptracker Pack to the Aquaria Setup Guides (#4037) * Adds Poptracker Pack to the Aquaria Setup Guides * Updates French Update Guide * Update worlds/aquaria/docs/setup_fr.md Co-authored-by: Cipocreep <65617616+Cipocreep@users.noreply.github.com> * Update worlds/aquaria/docs/setup_fr.md Co-authored-by: Benny D <78334662+benny-dreamly@users.noreply.github.com> * Update setup_fr.md * Update setup_fr.md --------- Co-authored-by: Cipocreep <65617616+Cipocreep@users.noreply.github.com> Co-authored-by: Benny D <78334662+benny-dreamly@users.noreply.github.com> --- worlds/aquaria/docs/setup_en.md | 15 +++++++++++++++ worlds/aquaria/docs/setup_fr.md | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/worlds/aquaria/docs/setup_en.md b/worlds/aquaria/docs/setup_en.md index 34196757a31c..8177725ded64 100644 --- a/worlds/aquaria/docs/setup_en.md +++ b/worlds/aquaria/docs/setup_en.md @@ -8,6 +8,8 @@ ## Optional Software - For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with +[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest) ## Installation and execution Procedures @@ -113,3 +115,16 @@ sure that your executable has executable permission: ```bash chmod +x aquaria_randomizer ``` + +## Auto-Tracking + +Aquaria has a fully functional map tracker that supports auto-tracking. + +1. Download [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) and +[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest). +2. Put the tracker pack into /packs/ in your PopTracker install. +3. Open PopTracker, and load the Aquaria pack. +4. For autotracking, click on the "AP" symbol at the top. +5. Enter the Archipelago server address (the one you connected your client to), slot name, and password. + +This pack will automatically prompt you to update if one is available. diff --git a/worlds/aquaria/docs/setup_fr.md b/worlds/aquaria/docs/setup_fr.md index 2c34f1e6a50f..66b6d6119708 100644 --- a/worlds/aquaria/docs/setup_fr.md +++ b/worlds/aquaria/docs/setup_fr.md @@ -2,9 +2,14 @@ ## Logiciels nécessaires -- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne) -- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases) +- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne) +- Le client du Randomizer d'Aquaria [Aquaria randomizer] +(https://github.com/tioui/Aquaria_Randomizer/releases) + +## Logiciels optionnels + - De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest) ## Procédures d'installation et d'exécution @@ -116,3 +121,15 @@ pour vous assurer que votre fichier est exécutable: ```bash chmod +x aquaria_randomizer ``` + +## Tracking automatique + +Aquaria a un tracker complet qui supporte le tracking automatique. + +1. Téléchargez [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) et [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest). +2. Mettre le fichier compressé du tracker dans le sous-répertoire /packs/ du répertoire d'installation de PopTracker. +3. Lancez PopTracker, et ouvrez le pack d'Aquaria. +4. Pour activer le tracking automatique, cliquez sur le symbole "AP" dans le haut de la fenêtre. +5. Entrez l'adresse du serveur Archipelago (le serveur auquel vous avez connecté le client), le nom de votre slot, et le mot de passe (si un mot de passe est nécessaire). + +Le logiciel vous indiquera si une mise à jour du pack est disponible. From 63d471514f05ccd4ccacc20807068400020f8bcc Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 16 Oct 2024 18:37:41 -0700 Subject: [PATCH 348/393] Pokemon Emerald: Add flag for shoal cave to bounces (#4021) * Pokemon Emerald: Add shoal cave state to map updates * Pokemon Emerald: Fix shoal cave flag wrong byte, delay bounce to end of map transition --- worlds/pokemon_emerald/client.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 4405b34074e0..5add7b3fca40 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -117,6 +117,11 @@ DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()} CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()} +SHOAL_CAVE_MAPS = tuple(data.constants[map_name] for map_name in [ + "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM", +]) + class PokemonEmeraldClient(BizHawkClient): game = "Pokemon Emerald" @@ -414,13 +419,17 @@ async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[st read_result = await bizhawk.guarded_read( ctx.bizhawk_ctx, - [(sb1_address + 0x4, 2, "System Bus")], - [guards["SAVE BLOCK 1"]] + [ + (sb1_address + 0x4, 2, "System Bus"), # Current map + (sb1_address + 0x1450 + (data.constants["FLAG_SYS_SHOAL_TIDE"] // 8), 1, "System Bus"), + ], + [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]] ) if read_result is None: # Save block moved return current_map = int.from_bytes(read_result[0], "big") + shoal_cave = int(read_result[1][0] & (1 << (data.constants["FLAG_SYS_SHOAL_TIDE"] % 8)) > 0) if current_map != self.current_map: self.current_map = current_map await ctx.send_msgs([{ @@ -429,6 +438,7 @@ async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[st "data": { "type": "MapUpdate", "mapId": current_map, + **({"tide": shoal_cave} if current_map in SHOAL_CAVE_MAPS else {}), }, }]) From ede59ef5a1c0d06e8eae0c7ad18b6d731f36450b Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Thu, 17 Oct 2024 09:40:46 -0700 Subject: [PATCH 349/393] WebHost: Fix NamedRange option dropdown being blank instead of custom when applying presets (#4063) --- WebHostLib/static/assets/playerOptions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WebHostLib/static/assets/playerOptions.js b/WebHostLib/static/assets/playerOptions.js index d0f2e388c2a6..fbf96a3a71c2 100644 --- a/WebHostLib/static/assets/playerOptions.js +++ b/WebHostLib/static/assets/playerOptions.js @@ -288,6 +288,11 @@ const applyPresets = (presetName) => { } }); namedRangeSelect.value = trueValue; + // It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom" + if (namedRangeSelect.selectedIndex == -1) + { + namedRangeSelect.value = "custom"; + } } // Handle options whose presets are "random" From af14045c3af1bc8d62cc02c953ef16ff016515e9 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Sat, 19 Oct 2024 16:53:02 +0200 Subject: [PATCH 350/393] Yacht Dice: Proguseful items: Dice and 100 Points #4070 --- worlds/yachtdice/Items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py index c76dc538146e..d6488498f51a 100644 --- a/worlds/yachtdice/Items.py +++ b/worlds/yachtdice/Items.py @@ -16,7 +16,7 @@ class YachtDiceItem(Item): item_table = { - "Dice": ItemData(16871244000, ItemClassification.progression), + "Dice": ItemData(16871244000, ItemClassification.progression | ItemClassification.useful), "Dice Fragment": ItemData(16871244001, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression), "Roll Fragment": ItemData(16871244003, ItemClassification.progression), @@ -64,7 +64,7 @@ class YachtDiceItem(Item): # These points are included in the logic and might be necessary to progress. "1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing), "10 Points": ItemData(16871244302, ItemClassification.progression), - "100 Points": ItemData(16871244303, ItemClassification.progression), + "100 Points": ItemData(16871244303, ItemClassification.progression | ItemClassification.useful), } # item groups for better hinting From c6d2971d67c48cbf14ec05e384fb044f4fe60a63 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 21 Oct 2024 01:51:14 +0200 Subject: [PATCH 351/393] WebHost: optimize WebHost theme PNGs (#4071) using zopfli, saving 30% --- .../cliffs/grass/cliff-bottom-left-corner.png | Bin 80038 -> 47918 bytes .../grass/cliff-bottom-right-corner.png | Bin 72277 -> 51317 bytes .../backgrounds/cliffs/grass/cliff-bottom.png | Bin 5516 -> 1831 bytes .../backgrounds/cliffs/grass/cliff-left.png | Bin 4876 -> 2956 bytes .../backgrounds/cliffs/grass/cliff-right.png | Bin 3873 -> 1477 bytes .../cliffs/grass/cliff-top-left-corner.png | Bin 57962 -> 36503 bytes .../cliffs/grass/cliff-top-right-corner.png | Bin 65049 -> 36421 bytes .../backgrounds/cliffs/grass/cliff-top.png | Bin 4225 -> 1454 bytes .../static/backgrounds/clouds/cloud-0001.png | Bin 20604 -> 7940 bytes .../static/backgrounds/clouds/cloud-0002.png | Bin 9739 -> 3922 bytes .../static/backgrounds/clouds/cloud-0003.png | Bin 6833 -> 2889 bytes WebHostLib/static/static/backgrounds/dirt.png | Bin 10341 -> 6068 bytes .../static/backgrounds/footer/footer-0001.png | Bin 16797 -> 14558 bytes .../static/backgrounds/footer/footer-0002.png | Bin 18888 -> 16269 bytes .../static/backgrounds/footer/footer-0003.png | Bin 19166 -> 16436 bytes .../static/backgrounds/footer/footer-0004.png | Bin 20304 -> 17307 bytes .../static/backgrounds/footer/footer-0005.png | Bin 19681 -> 16769 bytes .../static/backgrounds/grass-flowers.png | Bin 8827 -> 4811 bytes .../static/static/backgrounds/grass.png | Bin 8411 -> 4277 bytes .../static/backgrounds/header/dirt-header.png | Bin 40256 -> 26002 bytes .../backgrounds/header/grass-header.png | Bin 40084 -> 25892 bytes .../backgrounds/header/ocean-header.png | Bin 24424 -> 9215 bytes .../backgrounds/header/party-time-header.png | Bin 25186 -> 9844 bytes .../backgrounds/header/stone-header.png | Bin 68049 -> 44849 bytes WebHostLib/static/static/backgrounds/ice.png | Bin 10301 -> 6329 bytes .../static/static/backgrounds/jungle.png | Bin 36475 -> 21381 bytes .../static/static/backgrounds/ocean.png | Bin 32733 -> 8768 bytes .../static/static/backgrounds/party-time.png | Bin 41482 -> 35971 bytes .../static/static/backgrounds/stone.png | Bin 234931 -> 121442 bytes .../static/static/branding/header-logo.png | Bin 6999 -> 3381 bytes .../static/static/branding/landing-logo.png | Bin 44905 -> 39769 bytes .../button-images/hamburger-menu-icon.png | Bin 5659 -> 1577 bytes .../static/button-images/island-button-a.png | Bin 255717 -> 209015 bytes .../static/button-images/island-button-b.png | Bin 214542 -> 174114 bytes .../static/button-images/island-button-c.png | Bin 298988 -> 255053 bytes .../static/static/button-images/popover.png | Bin 9836 -> 3957 bytes .../static/static/decorations/island-a.png | Bin 165403 -> 127644 bytes .../static/static/decorations/island-b.png | Bin 164508 -> 126746 bytes .../static/static/decorations/island-c.png | Bin 166466 -> 128501 bytes .../static/decorations/rock-in-water.png | Bin 1949 -> 1298 bytes .../static/static/decorations/rock-single.png | Bin 1793 -> 258 bytes 41 files changed, 0 insertions(+), 0 deletions(-) diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png index 326670b7ebc46ebacaf61636af6255986f2be2a4..2435222d24f2000f673d3434fdcbb80831ad5928 100644 GIT binary patch literal 47918 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Ð|6}r9d?L|;_cd3 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 z

tu#znSA*ds|$t+=nyfcCm1OZQwdSJ7>DVY1%2A6 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 afRtRbCm6}E5(oSa$a5(LSfzwv;Qt2)!E7A> literal 72277 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 diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png index 2a28958e09311fb4699fb815870f36d7e22dcd26..7e58e4b0ce48ea3e8b3f34269c61e3de75eab933 100644 GIT binary patch literal 1831 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 a|I07LPbH_d+X#OzH8wncFx}wH#s33@v0lpn diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png index 9bc84ff603ec6340a6ad03c6840cb160a9a24ebf..2dabbf298678f5d76e33d7e8d18bbea65950c07d 100644 GIT binary patch literal 2956 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 N44$rjF6*2UngAG$MbrQQ literal 3873 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 V^eSJHYXRWL%G}nhXtT$Ge**qlK{@~c diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png index a40bca60f0808721bf393572a082dc68027dc083..2b5865573ad8fae6b88ee8958c61c5d5525bb141 100644 GIT binary patch literal 36503 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 RKY7Uj1fH&bF6*2UngG+uGK2sC literal 57962 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

C-Yg49>I6R`bGA!0Tyo}|(s z0sZvckE%ovZ5CPe!q(lZfl~wEP>Y#hS#=ZC4#{#ZVVi2`Xuq#f;*wDkr4b-UO*qfR zQPe$baL=oNB-U;*B%y{-HI3sTTzks2QguRk`#Cqmp^5^ySBy(Z{`wzH{T*wsi}%cc zOQ#F}(KQAPCM1a2#A8S#41F)xX!u~mHG`-xLQ8Rj3yF?;v4b!JW+9M62t%60T6oGZ^=Y z?1lJgg9{29(=8;x(C(#QX6v0-vj+f5Tq-{Qco@=Ihl15A*quYA{ESk+F01Dt>Ul>1 z9<08$=OxGSnXs7s{%e2l?QHO;MJpLLGZs^mUrOUCYpSBPGXC_k*?BLs@9FX}L>5UE zj@4->aLWE4P2U|)_51#>LRsM$$sXr~gUlis(ZO+yjB|`+OSWtg%0BkNL1d5Yk(Hgj zSN6`z-g|#v-oMY!za9_w%XMG(HJ{Jx3T|Phd4}Ow4*GT3^@&o@FRUMeaox)SQtg7} zJ{ofLTHu)oK%Q4qBPMXeYK-(bxu?$8_ z!i6e6*KoNY&)P4I9m9u)8z_JyCycrqtRW!p;s)VV8g@)dkW#Aq)vl7AC%|XJ>u$+b zefp}~RQHqWePssox3a1PTrS7>jg7B~e(k{=(p(!vsIVzaHE5ZoE#-+&-`8sG85_ME zepZ}_d7!HJoebdInF26n3mt~#(4~J5r46y3Flm&fwT$#YfeO&*RB6o`5Px@$HT>@X z-l*jx50wAq*EPC_+gh@!=eE*(Afi$x1%{YMWSO&Jl{`=puiyAr_n@_j=SKN~jEEH@ z`6?LH(v;y1FTPT`7wyle&h_vq z3UX7h!QNX>U>_O7AP*VGtBSMc{{+fHM*EZ7TmUZ&47hAgZmhh%nww*x;`Q2c0jPmf zWw8ma;?ThdN;TTkL23ie02I-Z?$fZv6~Pxx86y^(B4R>c_=6tb+g6qN=|FS)UTT2t z6g6Q{jF;{&bJC@1FbfweOGoAbM=TcrI|{`04RHyyN%2v!3Wf!H@_Vb25!{sY8oyGH z@jl_$RT63lxK(Ci{7-W;$>-cm7vw3?a_?IcIh6x;UZMvMhr`}m79}^YpIP2}8RmZ6 zO8)SEM89yXdF|c+E92$3k9g4q@AXi_@AE%Yi+yul%S5hwHTtsu{%CVgyHd`bCwc|j z57wIz_j)CTjPp`Zs6NINJLne+@ufv(U?NBxae{pxH#39031wb*2tK&~%!F&N5VqjK zBxW(q5C*#^>zkH!PjatbM{Gh)dXMXYlB_EHUr6wHE*z|(E^@UfoGKh0Sq=r^>Kb!( ze$VtgL|*=0iBf^;dmW1a$*>x@BUoaU?%6Ojp=d69+%cA^UK?nx+0og&6=wZ~<>KcT zHmAk@+Mk^Ac^LuPO-=UTb)G4vD-u6~CY{X&Yw5}d2kib$KRK$k2tw4J?K`}TF&}{{ zOP+bu?jMqhD)X$yaNdVCb!NlFg|bxf`6R25BtS3!6&Hp~#WxMP`x;gAh@f%vGbQPD z##LtA7MO;>XfMX2&3!@%lhQZlcAq>ufR*wrEeuiqe3X?X;x5SBg^NS7$VS;(v=u!{ zEXbktME0v1W4way7t-&dqh(fGN0lmF+FVhqxRvxXfR(koyryXD=L%15Q=fAuAKIfi zo~dnEDY)JZzxGft1bQDFP;!;chCIMTu;ZtP5-;>QXpbc|XD3QGZdCH%43Zq?g{qtLz`s)t&3l`rYn=TB!lwNmVQx`B} zw5GCOjU}uUxBz{0FX9@wR%RlcEM89#Y^eD;EmP%?4dnOdUoTPowh#pYv!OtyI^V

~vHERXxv+bf9Lvejb*gR3vp#5u*|c9yXCJ1p{!~ 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 literal 65049 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 LLm}m0lc4_(0*P(+ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png index bb6ccec3d583e95d190abad4691af7ebda8203fe..1eb072ac085897650622ffbd0859dcc07b261842 100644 GIT binary patch literal 1454 zcmb`GdsNbA7{^Jbt|qRnRT~{OGxHW83I-?$3f?c}C2vWJH$u%WXu`rG(n?d8AUX~G zc|)z#oMmOUny%9cvry2PrJkjkmc~+xy!>Rp&e>W2?9YAvc;4^xoacPc`@GM2&+QKI zM65%ugTY`3Z!dZfbn;=aHNr39kgBK)y$L0kcKd}e2uZE>)NBkzg!MYVYrotn62Q2g z#xl-121*?Li?H-_p6vRlv%kejAG0e4LXO{z7560ktW3J7N+^BuPb}+8kUmN|)mGj$ zUfuJiq;oVq|8{+!rb;ormt86QeWK$2OZgLxT&aPILGc$OlB+`#EB3^F5*dY^83mm} zv5btzq;fL_c@pdn(?m{6jbf6OkQfpaD7!Z#yE~X)+s1Gu`7me>R%STst&Y}aq@CLW zi8k(7OETJm;b7^Fw~C;mvg646J?tah&;=Ye%GrHG0^~B)k*8fed%0DX_~n)lH|;O@`fB+uB>uW!A68W|0t+wS5M7aYG)LeL&_PQ`3 zKeidLIOuTY=?^1rIoSYBv}t-$e&Wv7Zl=?Hve@`xN7#Uy$N*f*_ei^=)JQy!UH#6s zQP>~Rxn+J$Zaw7SZ_F<;&SbKgCQ5*2*!fGJ;;0ckni9EDY%ua_d-2+9FL-r4U^b*m z0noI?C?!BPK;qa+VA(KJG`OHV^xUgb-S8s+{lomPJ^dwsRp@g`Fh7#ftF}ZS7iRW7 zm#8oaS!j+>HH<0%)8rUc|B!Bzi=ejW^@(ml!2{wHwA3Z0at%lpRYPx}`r$ zPy%)66ktOU9J&X)g!>cMyW-8-exU(kcneBR%ZSzw0`lZ7DqdY$Gf%(&22fdD{&t4r zR0ZlIxoXh8L)$)d#!%`tPZ{p7Y@UavS$Kt!3h)5gu)}yNjfX$pvzA+|8iU?iRtY*9 ie45q=IPtyQQ>Qn#oJ^zx*mUSC!@L;*^k%o1Q-1?y4fh2A literal 4225 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 F`9Desy>0*i literal 20604 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 diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png index 33f09b19ce8615db5e1849f8baa65c2c8b290be7..7b479bfe7b0b38253f01ee99b9abbd01fc0c6e54 100644 GIT binary patch literal 3922 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 diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png index f665015b0d01628b726abc0f10854ec70a55e620..59844e31ac42dbc5717e3875c7ae76bd2c4e4578 100644 GIT binary patch literal 2889 zcmb7GdpuO>8=r1+t8DH{DkZ-#D`l}o+Hwyy*invLk~WItGL%rtq}B@cvu@Fav@>$# z5(l{rqar0@77a73m>Ji`ka3xP&#<50{`)(h_jBew&->h;@B2J+=Y)gR25B{E91gd^ z#`+imhg*fi=XeZyp!D_6>S;gpPyL3>B>*#==s{dwtMY|F!J)R(6I$ zx;!4DJw2~=|Lt{XD7WxUN^@odQ8E=SpzVfeWF0>=Rz?|c46Ty~LOSNC^Y23?CKb&! zKWToRET;iwcnHu#&}k^WQW`N3L_VI9WmVh6+M!R#GATxPU@2g7$JfyqJ2aTwa$uHx zGT79AJoB2@DX$gPDc?g8zQtU6PzW>odGlLe7uC)|VA-5wq(c~*to67{aPvZ8HjRuq zf^7tm-OB&v(TjB-hBP?k+F~@dfwPO{H$$q8IaTa~50iTumYJJWFbP%OKywmfZxFEP z9qJ#*EDNeGE$7~vtJ3oo>m%8lK4hPDC`&8QgV5ku8De@w7!f_A?ernd$inZ2j+@xJ zG-D~$h3wCQ*+LnlcQJaXV5j9Zy5f*?UH{DNtmv+eG^0LL9hsWl1gTCazpVUfBt;Vh zcZRPgN+Qk)GQ?6*efDcbih(qPS&4Ym8~WmPWQj>wT>h;k`97Epl5u7$OlJ2u?nPnx zCgQ7bb(ky-g8mQG(_rX|F4;&D^Wat~cC#EpggK3dcgw}Nmup)^8xePh^fST5o@5?QTI69Gm5;xgx+GRWnHJ4%6er)j!hMgH|9ndE&7gaCbCSd495VZh_}rC zJc(41)a{>-&%??UgnBfO@*SKNPDxx1v;Dec4>pT&r}2#W1_z-(xSvAdb&`x%6NZrE z%+;6U)&6YTPSoJJwitF~oA-ZC@gYa#pKbFMC^S%aXh?~nN=y1)d{c6;a2 zugsv4isk~95Kc#El1SSS51p*|!h=nZ@SWAY?AfPF05o)*&Xc&(l`}WLG{kLy{>GpCM6pt8bhaN{FfW z(!EC&LyDn!`2K4CiKl(94MSa;TJPyQUB-e?`}%IgUqE$*rpnT+rgotLxZL;|5eaSj zKytSR=gs}S&nSgu2|D&64Ek{8X9G#%Qd}L*6+7zVd~4dmU)a)wJ_#Dq@MO_aye@xJ zL!iYy4I)o|{+A4a`Z$j0r}`jYg$f{C)1-J)r=gif=Jk0Oee*R&{LCKKR+%`NdbnKr zVb39BW0h6JNvlRKHCysL-P}Fb{f!*k))|F|=GKq)xn3hb?%Viz5})%UbA$!q z@@-X}LwT7q4HrATd=Ol@b!*&}-M{>tyUdNa4+@KTm5Hzgn>%2~{*|#9`p%a{71V!t zHeU7W*@ffA+p^SsK>F6DX+-aDb-rcdC4qOmUGY#N2&>)E!+H*|h2I^_tVIkjw1T@f z^?c)N(fJ2Gk7iO1iNLiB8(EE^ksQauIvH)#d z&5~BZhqep^qwtO`cl|XOFWBv%7u4om7*9LsH(!}GEBxb9Z_Z9@%dPI;VfSrlWM_Mz zGGds5CEqSeOYM!g8Q$&kL3jrf4}=qLVlIfQh()~8Isx-PZ4>=x)}39MTUdGl;QBQA zaTzs<)uY^VL=$-j&$$~E(w&+rc4{|4RqFJJx# z;2vkAfV{9yi^qzYrt&~!Wx$_l{UxzGa+=j~z~F5iNKgC1i~W0KExufk!?HF+2U9p! z$9tGbO@vJiHmSQ`rR25wLP`q2#;3?j%I)&iQJW!};#c3%!ordFT)ubOq3x$Zdt?(j z5TUlR%iWIs+~Wq~bdyc2b^um^W|7C>ug*6?DS!{`=52U3;cRKV9kAnSb#45)eYjp= zZ}*w97qC=nbR@NRRn2JS_!AU)@FuKAEfuD8k3@egtge>3KsF@9QjD&!k6@xCK8+~h zRr0+RC`Djmro!Y`EG^O;K7*v8Jb3(zeH0pNSMa@wX{l(n(p$?PIgAcF@V5|WW8rWN I`N{Lf-{*H16aWAK literal 6833 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 literal 16797 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 literal 18888 zcmeIacUY5I*EbrS8J!swL=mJbqJT6h(u;*E0)o;@6r^_uy@VM?MCu^Y2?(JmRhrZg zlqOw3I!O?uh7cfxKthsp2X%bjbH4Yv-t+zOe%CqIyZ#_|+4o-S-h1t}erv6L^X#6! z)~Vk)e+Pj;r*yQ{jXK*gi1nv!22o)^oIyrfx#zCH$Ol`E%`Z&tWjh08rc`0N{^;B<Oqh0yNEBapFNAlfk zyNmbRwV=$9>W~P3K%W1h|Hcup3jS|o_CKutKQaPsg~$ItBI*C1#UvEH(L8*=96XV} zXWA>9w?9h9flXsiPrvn!{v}2u+1%F8-1Z32X8GnVZbs8KUpKUQr3EQ3wKKB(DYKkJ zaHt%mf511N$Qf4AuhpJqoiDG2G;b9B#Wm}Wh!XRCh_SPP4uvCo4qcO zO6CMigiZ`hje4{79?Q_%xAm3W^$8&Xagk+v`?HPFU%_ z$0t}N{o2U8g4P>-t&@zl&Y1%@-Rhr#9?c$RadBayuZGFj{{@hWV($*AzgQc-2-n#qobmAg6iV0OCd?y4y| zE>bRcF-HFHpMZf9ZF-?PS^HNXAA$wNmGR~E*=xAsU~-C_r9CR%#xj;xzPK>-GRt-c zd%Z^$uS?PBxEY3-8#KivAaJB=g4zD3zHU!D^>vj{ZY40~rmRUH(nmxQz7W99-mViD zj!;c?u8f)7zv0ho(0YKH-^3y8Do67zyVi=g{gi3jVAh5D084i{k=#(>8O#jARD*64 zPN~d6$MKKJ*)5lPvWLvzd75FUl<+cI%FL8hB61Ig-rEY7%`5Mj24g#JbEnaE!}Gk>UW5y_@sv?6{2 z&G}5X)P&221>|{rRw||D(~V-&(uzj*kv@Mjl}yGF4?YapZ;b&qOfB3Y{!iUoKWvfX z<*d>XbDoZs+pg^4_79pDR-@o!<{-`3<-vZ~aX^Mmw`#txF~G=p~c{Fo$p7-b+bcf)^4V`P$iu zjQ+hf-u|@Gx+sv*@Q@s}ZdpajRH){P0EVJ-L;~?QOb1^Vy}h<|e4&)rz_w+~Bb_=w zEg#H9H6fT~TE)H#F?B6Y#7sX!l?UM8mM~ULMB{kl2o}7*XgPI-DZHE=krq zgqVVzuP4&5s1-zT6ONnuv_1O8+KIBVuFVaXQpKi94Ouj5!X%$QBBYe)^QlK4AJ!7R zF>p7)Cj%cM8f70BUV3p{L_(ct(yDDD!HE?z1;^Be9JG$-gVmR9SR{1}@7^94;V&vB z6_0y2N;zc@jd(KEj4JW9@QN6Ca#I5`?gF=G zp=sBM+fBvsJtYR-+3VJB$wFKVj7^ z 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 diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.png b/WebHostLib/static/static/backgrounds/footer/footer-0003.png index 5fc31d1ee970b40fe59c1a5d42c0f638b87be42f..9c4d9a4632b05dd7b026119103058566778fa2e9 100644 GIT binary patch literal 16436 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 literal 19166 zcmeIaXIPV2v@jZ-QN}?;6zN655flXJNQ;F44hn+OOB4m9_g;eI2uwm5q)CY=y(7Jb z;LxNSA~l52dk8&{kmSC>Dd+t7<~jHL`0jJ=bM}vz{kq?^)?R(By~A%0^|aZ3 z0qU<6V!IMVGTa{DRj$?sxZmX zt=Fu-eDOq?>t23PSk!3V6e&07ZWq|Fnv~)ZCD{+3shHKQ$docOLkmB=l{Nc3{XBF+ zKiESAAm@L_|3wZ+ssCR@_P?n9hj3tO@VoyZl>Yyi%rM9!Q_-XS4UostR9)M&FNX(= zJ?SFbA6#(CZRgrEyVS}XB+G#%VcQokonbDWx>>XC`nZJpuxDm^FH75kTfN!BPstD$ z;0&Iu=cHl0+8yuJAg;L7re#{tmDp>9>9cYUlE90)S-Tb?Ob8%Q-jwa_69A~ipT5Cl zJ`2nF6TyFf^3XIo&uw*G%N%~=lriVTO;Tr^VRk#`Sl>j`xltl}!T86T2wOxknAm@Nwh(W$W++4-{5QV2IU zaGl!cMxEqx$4Sm&r0>2x(`|Ap7xK^FaLj!IKJpBBScBhHg# zLDGcZzhmh*;+IjlQ=hy0t}tuvlkq8!2R^5vW{62*;Q}{>|W*>0q8s*1w+q_v8tPkT`*^J(!n&kkQqbE#Q+Z@^#t$ zoI`2_q(+#eQ}I0j-qgk-EsYR$EkwEYSLALeC#t{T9n%*~$h zU6D*vqc!-IY^G_S()6-PC?}SDr5amgKbya|nm4fHdZrTl=voCnw(`JapkplDw~SrI zVAzisT~i7ny*9{=y9$45MCfp6iwB3wjr{sn}E zuDn5JP169{w<2wgSZAfJslB6D3U_#yHS-QtdKB)7mDm_~_c%)qR`F?b5;S}TD;-tZ zQ4>4W`h*yOzl-|CB7?VU&^IR!X3;Lej)E?vu;)(G)F#=1obHwwD3`pPr26?0m}o z$&xnOw=~&%Ssxi@1|GteHDj)=y6(iSV$i6-LtgJkQK2PQ#uh`C-$yDJllUiS;}+%U zZC76*r;N~peV3?6nzp_Tc@^B-;kcXZs%`ImxISZ}9E~X(>Xap#a5M^k?w>`K1()%l zt6^(C=H5M~4DaR`G3Y^&&DOAW5z&tLNZOVf)JQH`A`G}OhOia}*%Z6AP-2Mmv-RmoU;U5I_P>iOA_wb*WbQiNj)qYTPBgs z?5R1bO5xGCb~As*+r*-K3>6TljdHkii>50yzKih|#&0@DgZ%AZ~r}b)m zKae+Xw47x>e6dbbiteYzv_8<2+mBhaAV3@o+rIp#Bh!NiGwnjzmf2m%U;#53#%%4u3Mc3^ct5Ee*ego*uuZ<23YOxd$~

hveUL-7vxYjA9pP|Gw;w@QMekObCA#-Hp+-04bd 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 if87`cT4I`Q43?n+=caR!d2p*&p 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 literal 20304 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 diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.png b/WebHostLib/static/static/backgrounds/footer/footer-0005.png index 7b7cd502f36c69a79b4efb210c30d7b87236998b..58093f7efcf25cb5383caeb2e2668daf03841da2 100644 GIT binary patch literal 16769 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 L>pJ4vo!|c#MTA_F literal 19681 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 P0?A1!NtTNn|Mh!uV~L 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 aI|iWA4PNw?)9XT>sO#$(U9Z086!|aYv1i!; literal 8411 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

FK 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�?m6 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 literal 40084 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 literal 24424 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∨LyJx3l>e+$VJz@5@^tX(@ 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 Sm)2_M1buxv;E~@y|NVcum}rv# diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.png b/WebHostLib/static/static/backgrounds/header/party-time-header.png index 799f32f2282ebbea85bfc5cfc233d4582a9f751b..b033486b2aea1e3703bf6bd87e1b39685e9f7547 100644 GIT binary patch literal 9844 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 literal 25186 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 H&xHR0AT-nh diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.png b/WebHostLib/static/static/backgrounds/header/stone-header.png index e0c9787e5735952a8fcde76945a95be7e347f142..6e93a54b78852607bea17e7e2cf1fb0371cb4b31 100644 GIT binary patch literal 44849 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 literal 68049 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 diff --git a/WebHostLib/static/static/backgrounds/ice.png b/WebHostLib/static/static/backgrounds/ice.png index fcf7299b35827fc8aa3672f01c77ae26252985e0..c64f1b20f3b0781a113e6250f03238d2ffc3b7e3 100644 GIT binary patch literal 6329 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 literal 10301 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 diff --git a/WebHostLib/static/static/backgrounds/jungle.png b/WebHostLib/static/static/backgrounds/jungle.png index e27d7e992086bc20039c271ccf4deac7aa5b1b66..c4ec5b9648479da161c68d8a9a3b3080df30424b 100644 GIT binary patch literal 21381 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