Merge branch 'main' into tunc-combat-logic

This commit is contained in:
Scipio Wright
2024-07-27 17:33:19 -04:00
committed by GitHub
69 changed files with 1916 additions and 451 deletions

View File

@@ -61,6 +61,7 @@ class ClientCommandProcessor(CommandProcessor):
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 @@ class CommonContext:
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()

13
Main.py
View File

@@ -124,14 +124,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player in multiworld.player_ids:
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
world_excluded_locations = set()
for location_name in multiworld.worlds[player].options.priority_locations.value:
try:
location = multiworld.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if location_name not in multiworld.worlds[player].location_name_to_id:
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
else:
except KeyError:
continue
if location.progress_type != LocationProgressType.EXCLUDED:
location.progress_type = LocationProgressType.PRIORITY
else:
logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.")
world_excluded_locations.add(location_name)
multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations
# Set local and non-local item rules.
if multiworld.players > 1:

View File

@@ -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

45
docs/apworld_dev_faq.md Normal file
View File

@@ -0,0 +1,45 @@
# 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](<adding games.md>), and [World API](<world api.md>).
---
### 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. 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
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()
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))]
```

View File

@@ -596,6 +596,7 @@ class GameManager(App):
def connect_button_action(self, button):
self.ctx.username = None
self.ctx.password = None
if self.ctx.server:
async_start(self.ctx.disconnect())
else:

View File

@@ -3,6 +3,7 @@ Application settings / host.yaml interface using type hints.
This is different from player options.
"""
import os
import os.path
import shutil
import sys
@@ -11,7 +12,6 @@ import warnings
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 @@ class Settings(Group):
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 @@ class Settings(Group):
# 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] = []

View File

@@ -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 TestSettingsDumper(unittest.TestCase):
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")

View File

@@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]:
continue
else:
if name == "Scooter Badge":
if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
item_type = ItemClassification.progression
elif name == "No Bonk Badge" and world.is_dw():
item_type = ItemClassification.progression

View File

@@ -659,6 +659,10 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region,
if exit_act.name not in chapter_finales:
return False
exit_chapter: str = act_chapters.get(exit_act.name)
# make sure that certain time rift combinations never happen
always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest"
if not ignore_certain_rules or always_block:
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
return False
@@ -684,9 +688,12 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
if act.name not in guaranteed_first_acts:
return False
if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name:
return False
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
start_chapter = world.options.StartingChapter
if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON:
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
if "Time Rift" in act.name:
return False
@@ -723,7 +730,8 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
return False
if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest":
if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \
and act_chapters.get(act.name, "") == "Subcon Forest":
# Only allow Subcon levels if painting skips are allowed
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
return False

View File

@@ -1,7 +1,6 @@
from worlds.AutoWorld import CollectionState
from worlds.generic.Rules import add_rule, set_rule
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
shop_locations, event_locs
from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
from BaseClasses import Location, Entrance, Region
from typing import TYPE_CHECKING, List, Callable, Union, Dict
@@ -148,14 +147,14 @@ def set_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
chapter_list.append(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
chapter_list.append(ChapterIndex.METRO)
chapter_list.remove(starting_chapter)
world.random.shuffle(chapter_list)
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
index1 = 69
index2 = 69
pos: int
@@ -165,7 +164,7 @@ def set_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
index1 = chapter_list.index(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
index2 = chapter_list.index(ChapterIndex.METRO)
lowest_index = min(index1, index2)
@@ -242,9 +241,6 @@ def set_rules(world: "HatInTimeWorld"):
if not is_location_valid(world, key):
continue
if key in contract_locations.keys():
continue
loc = world.multiworld.get_location(key, world.player)
for hat in data.required_hats:
@@ -256,7 +252,7 @@ def set_rules(world: "HatInTimeWorld"):
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
if data.hit_type != HitType.none and world.options.UmbrellaLogic:
if data.hit_type == HitType.umbrella:
add_rule(loc, lambda state: state.has("Umbrella", world.player))
@@ -518,7 +514,7 @@ def set_hard_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.ICE))
# Hard: clear Rush Hour with Brewing Hat only
if world.options.NoTicketSkips is not NoTicketSkips.option_true:
if world.options.NoTicketSkips != NoTicketSkips.option_true:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
lambda state: can_use_hat(state, world, HatType.BREWING))
else:

View File

@@ -1,15 +1,16 @@
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
calculate_yarn_costs
calculate_yarn_costs, alps_hooks
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
get_total_locations
from .Rules import set_rules
from .Rules import set_rules, has_paintings
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from Utils import local_path
@@ -86,19 +87,27 @@ class HatInTimeWorld(World):
if self.is_dw_only():
return
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter)
# Take care of some extremely restrictive starts in other chapters with act shuffle off
if not self.options.ActRandomizer:
start_chapter = self.options.StartingChapter
if start_chapter == ChapterIndex.ALPINE:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if self.options.UmbrellaLogic:
self.multiworld.push_precollected(self.create_item("Umbrella"))
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
if not self.options.ActRandomizer:
if start_chapter == ChapterIndex.ALPINE:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if self.options.UmbrellaLogic:
self.multiworld.push_precollected(self.create_item("Umbrella"))
if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings:
if self.options.ShuffleAlpineZiplines:
ziplines = list(alps_hooks.keys())
ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one
self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines)))
elif start_chapter == ChapterIndex.SUBCON:
if self.options.ShuffleSubconPaintings:
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
elif start_chapter == ChapterIndex.BIRDS:
if self.options.UmbrellaLogic:
if self.options.LogicDifficulty < Difficulty.EXPERT:
self.multiworld.push_precollected(self.create_item("Umbrella"))
elif self.options.LogicDifficulty < Difficulty.MODERATE:
self.multiworld.push_precollected(self.create_item("Umbrella"))
def create_regions(self):
# noinspection PyClassVar
@@ -119,7 +128,10 @@ class HatInTimeWorld(World):
# place vanilla contract locations if contract shuffle is off
if not self.options.ShuffleActContracts:
for name in contract_locations.keys():
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
loc = self.get_location(name)
loc.place_locked_item(create_item(self, name))
if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well":
add_rule(loc, lambda state: has_paintings(state, self, 1))
def create_items(self):
if self.has_yarn():
@@ -317,7 +329,7 @@ class HatInTimeWorld(World):
def remove(self, state: "CollectionState", item: "Item") -> bool:
old_count: int = state.count(item.name, self.player)
change = super().collect(state, item)
change = super().remove(state, item)
if change and old_count == 1:
if "Stamp" in item.name:
if "2 Stamp" in item.name:

View File

@@ -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.

View File

@@ -554,7 +554,8 @@ class HKWorld(World):
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

View File

@@ -9,7 +9,7 @@ from worlds.AutoWorld import WebWorld, World
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)
@@ -167,7 +170,8 @@ class LingoWorld(World):
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 = {

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1478,3 +1478,145 @@ progression:
Progressive Art Gallery: 444563
Progressive Colorful: 444580
Progressive Pilgrimage: 444583
Progressive Suits Area: 444602
Progressive Symmetry Room: 444608
Progressive Number Hunt: 444654
panel_doors:
Starting Room:
HIDDEN: 444589
Hidden Room:
OPEN: 444590
Hub Room:
ORDER: 444591
SLAUGHTER: 444592
TRACE: 444594
RAT: 444595
OPEN: 444596
Crossroads:
DECAY: 444597
NOPE: 444598
WE ROT: 444599
WORDS SWORD: 444600
BEND HI: 444601
Lost Area:
LOST: 444603
Amen Name Area:
AMEN NAME: 444604
The Tenacious:
Black Palindromes: 444605
Near Far Area:
NEAR FAR: 444606
Warts Straw Area:
WARTS STRAW: 444609
Leaf Feel Area:
LEAF FEEL: 444610
Outside The Agreeable:
MASSACRED: 444611
BLACK: 444612
CLOSE: 444613
RIGHT: 444614
Compass Room:
Lookout: 444615
Hedge Maze:
DOWN: 444617
The Perceptive:
GAZE: 444618
The Observant:
BACKSIDE: 444619
STAIRS: 444621
The Incomparable:
Giant Sevens: 444622
Orange Tower:
Access: 444623
Orange Tower First Floor:
SECRET: 444624
Orange Tower Fourth Floor:
HOT CRUSTS: 444625
Orange Tower Fifth Floor:
SIZE: 444626
First Second Third Fourth:
FIRST SECOND THIRD FOURTH: 444627
The Colorful (White):
BEGIN: 444628
The Colorful (Black):
FOUND: 444630
The Colorful (Red):
LOAF: 444631
The Colorful (Yellow):
CREAM: 444632
The Colorful (Blue):
SUN: 444633
The Colorful (Purple):
SPOON: 444634
The Colorful (Orange):
LETTERS: 444635
The Colorful (Green):
WALLS: 444636
The Colorful (Brown):
IRON: 444637
The Colorful (Gray):
OBSTACLE: 444638
Owl Hallway:
STRAYS: 444639
Outside The Initiated:
UNCOVER: 444640
OXEN: 444641
Outside The Bold:
UNOPEN: 444642
BEGIN: 444643
Outside The Undeterred:
ZERO: 444644
PEN: 444645
TWO: 444646
THREE: 444647
FOUR: 444648
Number Hunt:
FIVE: 444649
SIX: 444650
SEVEN: 444651
EIGHT: 444652
NINE: 444653
Color Hunt:
EXIT: 444655
RED: 444656
BLUE: 444658
YELLOW: 444659
ORANGE: 444660
PURPLE: 444661
GREEN: 444662
The Bearer:
FARTHER: 444663
MIDDLE: 444664
Knight Night (Final):
TRUSTED: 444665
Outside The Wondrous:
SHRINK: 444666
Hallway Room (1):
CASTLE: 444667
Hallway Room (2):
COUNTERCLOCKWISE: 444669
Hallway Room (3):
TRANSFORMATION: 444670
Hallway Room (4):
WHEELBARROW: 444671
Outside The Wanderer:
WANDERLUST: 444672
Art Gallery:
ORDER: 444673
Room Room:
STAIRS: 444674
Colors: 444676
Outside The Wise:
KITTEN CAT: 444677
Outside The Scientific:
OPEN: 444678
Directional Gallery:
TURN LEARN: 444679
panel_groups:
Tenacious Entrance Panels: 444593
Symmetry Room Panels: 444607
Backside Entrance Panels: 444620
Colorful Panels: 444629
Color Hunt Panels: 444657
Hallway Room Panels: 444668
Room Room Panels: 444675

View File

@@ -12,6 +12,11 @@ class RoomAndPanel(NamedTuple):
panel: str
class RoomAndPanelDoor(NamedTuple):
room: Optional[str]
panel_door: str
class EntranceType(Flag):
NORMAL = auto()
PAINTING = auto()
@@ -63,9 +68,15 @@ class Panel(NamedTuple):
exclude_reduce: bool
achievement: bool
non_counting: bool
panel_door: Optional[RoomAndPanelDoor] # This will always be fully specified.
location_name: Optional[str]
class PanelDoor(NamedTuple):
item_name: str
panel_group: Optional[str]
class Painting(NamedTuple):
id: str
room: str

View File

@@ -3,7 +3,7 @@ from typing import Dict, List, NamedTuple, Set
from BaseClasses import Item, ItemClassification
from .static_logic import DOORS_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, get_door_item_id, \
get_progressive_item_id, get_special_item_id
get_progressive_item_id, get_special_item_id, PANEL_DOORS_BY_ROOM, get_panel_door_item_id, get_panel_group_item_id
class ItemType(Enum):
@@ -65,6 +65,21 @@ def load_item_data():
ItemClassification.progression, ItemType.NORMAL, True, [])
ITEMS_BY_GROUP.setdefault("Doors", []).append(group)
panel_groups: Set[str] = set()
for room_name, panel_doors in PANEL_DOORS_BY_ROOM.items():
for panel_door_name, panel_door in panel_doors.items():
if panel_door.panel_group is not None:
panel_groups.add(panel_door.panel_group)
ALL_ITEM_TABLE[panel_door.item_name] = ItemData(get_panel_door_item_id(room_name, panel_door_name),
ItemClassification.progression, ItemType.NORMAL, False, [])
ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name)
for group in panel_groups:
ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), ItemClassification.progression,
ItemType.NORMAL, False, [])
ITEMS_BY_GROUP.setdefault("Panels", []).append(group)
special_items: Dict[str, ItemClassification] = {
":)": ItemClassification.filler,
"The Feeling of Being Lost": ItemClassification.filler,

View File

@@ -8,21 +8,31 @@ from .items import TRAP_ITEMS
class ShuffleDoors(Choice):
"""If on, opening doors will require their respective "keys".
"""This option specifies how doors open.
- **Simple:** Doors are sorted into logical groups, which are all opened by
receiving an item.
- **Complex:** The items are much more granular, and will usually only open
a single door each.
- **None:** Doors in the game will open the way they do in vanilla.
- **Panels:** Doors still open as in vanilla, but the panels that open the
doors will be locked, and an item will be required to unlock the panels.
- **Doors:** the doors themselves are locked behind items, and will open
automatically without needing to solve a panel once the key is obtained.
"""
display_name = "Shuffle Doors"
option_none = 0
option_simple = 1
option_complex = 2
option_panels = 1
option_doors = 2
alias_simple = 2
alias_complex = 2
class GroupDoors(Toggle):
"""By default, door shuffle in either panels or doors mode will create individual keys for every panel or door to be locked.
When group doors is on, some panels and doors are sorted into logical groups, which are opened together by receiving an item."""
display_name = "Group Doors"
class ProgressiveOrangeTower(DefaultOnToggle):
"""When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up.
"""When "Shuffle Doors" is on doors mode, this setting governs the manner in which the Orange Tower floors open up.
- **Off:** There is an item for each floor of the tower, and each floor's
item is the only one needed to access that floor.
@@ -33,7 +43,7 @@ class ProgressiveOrangeTower(DefaultOnToggle):
class ProgressiveColorful(DefaultOnToggle):
"""When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up.
"""When "Shuffle Doors" is on either panels or doors mode and "Group Doors" is off, this setting governs the manner in which The Colorful opens up.
- **Off:** There is an item for each room of The Colorful, meaning that
random rooms in the middle of the sequence can open up without giving you
@@ -253,6 +263,7 @@ lingo_option_groups = [
@dataclass
class LingoOptions(PerGameCommonOptions):
shuffle_doors: ShuffleDoors
group_doors: GroupDoors
progressive_orange_tower: ProgressiveOrangeTower
progressive_colorful: ProgressiveColorful
location_checks: LocationChecks

View File

@@ -7,8 +7,8 @@ from .items import ALL_ITEM_TABLE, ItemType
from .locations import ALL_LOCATION_TABLE, LocationClassification
from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition
from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, \
SUNWARP_ENTRANCES, SUNWARP_EXITS
PANELS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, PROGRESSIVE_DOORS_BY_ROOM, \
PANEL_DOORS_BY_ROOM, PROGRESSIVE_PANELS_BY_ROOM, SUNWARP_ENTRANCES, SUNWARP_EXITS
if TYPE_CHECKING:
from . import LingoWorld
@@ -18,6 +18,8 @@ class AccessRequirements:
rooms: Set[str]
doors: Set[RoomAndDoor]
colors: Set[str]
items: Set[str]
progression: Dict[str, int]
the_master: bool
postgame: bool
@@ -25,6 +27,8 @@ class AccessRequirements:
self.rooms = set()
self.doors = set()
self.colors = set()
self.items = set()
self.progression = dict()
self.the_master = False
self.postgame = False
@@ -32,12 +36,17 @@ class AccessRequirements:
self.rooms |= other.rooms
self.doors |= other.doors
self.colors |= other.colors
self.items |= other.items
self.the_master |= other.the_master
self.postgame |= other.postgame
for progression, index in other.progression.items():
if progression not in self.progression or index > self.progression[progression]:
self.progression[progression] = index
def __str__(self):
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
f" the_master={self.the_master}, postgame={self.postgame})"
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}, items={self.items}," \
f" progression={self.progression}), the_master={self.the_master}, postgame={self.postgame}"
class PlayerLocation(NamedTuple):
@@ -117,15 +126,15 @@ class LingoPlayerLogic:
self.item_by_door.setdefault(room, {})[door] = item
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
progression_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
if room_name in PROGRESSIVE_DOORS_BY_ROOM and door_data.name in PROGRESSIVE_DOORS_BY_ROOM[room_name]:
progression_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name
progression_handling = should_split_progression(progression_name, world)
if progression_handling == ProgressiveItemBehavior.SPLIT:
self.set_door_item(room_name, door_data.name, door_data.item_name)
self.real_items.append(door_data.item_name)
elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
progressive_item_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name
self.set_door_item(room_name, door_data.name, progressive_item_name)
self.real_items.append(progressive_item_name)
else:
@@ -156,17 +165,31 @@ class LingoPlayerLogic:
victory_condition = world.options.victory_condition
early_color_hallways = world.options.early_color_hallways
if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
raise OptionError("You cannot have reduced location checks when door shuffle is on, because there would not"
" be enough locations for all of the door items.")
if location_checks == LocationChecks.option_reduced:
if door_shuffle == ShuffleDoors.option_doors:
raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when door shuffle"
f" is on, because there would not be enough locations for all of the door items.")
if door_shuffle == ShuffleDoors.option_panels:
if not world.options.group_doors:
raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when ungrouped"
f" panels mode door shuffle is on, because there would not be enough locations for"
f" all of the panel items.")
if color_shuffle:
raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both"
f" panels mode door shuffle and color shuffle because there would not be enough"
f" locations for all of the items.")
if world.options.sunwarp_access >= SunwarpAccess.option_individual:
raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both"
f" panels mode door shuffle and individual or progressive sunwarp access because"
f" there would not be enough locations for all of the items.")
# Create door items, where needed.
door_groups: Set[str] = set()
for room_name, room_data in DOORS_BY_ROOM.items():
for door_name, door_data in room_data.items():
if door_data.skip_item is False and door_data.event is False:
if door_data.type == DoorType.NORMAL and door_shuffle != ShuffleDoors.option_none:
if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple:
if door_data.type == DoorType.NORMAL and door_shuffle == ShuffleDoors.option_doors:
if door_data.door_group is not None and world.options.group_doors:
# Grouped doors are handled differently if shuffle doors is on simple.
self.set_door_item(room_name, door_name, door_data.door_group)
door_groups.add(door_data.door_group)
@@ -188,7 +211,29 @@ class LingoPlayerLogic:
self.real_items.append(door_data.item_name)
self.real_items += door_groups
# Create panel items, where needed.
if world.options.shuffle_doors == ShuffleDoors.option_panels:
panel_groups: Set[str] = set()
for room_name, room_data in PANEL_DOORS_BY_ROOM.items():
for panel_door_name, panel_door_data in room_data.items():
if panel_door_data.panel_group is not None and world.options.group_doors:
panel_groups.add(panel_door_data.panel_group)
elif room_name in PROGRESSIVE_PANELS_BY_ROOM \
and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[room_name]:
progression_obj = PROGRESSIVE_PANELS_BY_ROOM[room_name][panel_door_name]
progression_handling = should_split_progression(progression_obj.item_name, world)
if progression_handling == ProgressiveItemBehavior.SPLIT:
self.real_items.append(panel_door_data.item_name)
elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
self.real_items.append(progression_obj.item_name)
else:
self.real_items.append(panel_door_data.item_name)
self.real_items += panel_groups
# Create color items, if needed.
if color_shuffle:
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
@@ -244,7 +289,7 @@ class LingoPlayerLogic:
elif location_checks == LocationChecks.option_insanity:
location_classification = LocationClassification.insanity
if door_shuffle != ShuffleDoors.option_none and not early_color_hallways:
if door_shuffle == ShuffleDoors.option_doors and not early_color_hallways:
location_classification |= LocationClassification.small_sphere_one
for location_name, location_data in ALL_LOCATION_TABLE.items():
@@ -286,7 +331,7 @@ class LingoPlayerLogic:
"iterations. This is very unlikely to happen on its own, and probably indicates some "
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \
if door_shuffle == ShuffleDoors.option_doors and location_checks != LocationChecks.option_insanity \
and not early_color_hallways and world.multiworld.players > 1:
# Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is
# only three checks. In a multiplayer situation, this can be frustrating for the player because they are
@@ -301,19 +346,19 @@ class LingoPlayerLogic:
# Starting Room - Exit Door gives access to OPEN and TRACE.
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
if not color_shuffle and not world.options.enable_pilgrimage:
# HOT CRUST and THIS.
good_item_options.append("Pilgrim Room - Sun Painting")
if not color_shuffle:
if door_shuffle == ShuffleDoors.option_simple:
if not world.options.enable_pilgrimage:
# HOT CRUST and THIS.
good_item_options.append("Pilgrim Room - Sun Painting")
if world.options.group_doors:
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
good_item_options.append("Welcome Back Doors")
else:
# WELCOME BACK and CLOCKWISE.
good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
if door_shuffle == ShuffleDoors.option_simple:
if world.options.group_doors:
# Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
good_item_options.append("Rhyme Room Doors")
@@ -359,13 +404,11 @@ class LingoPlayerLogic:
def randomize_paintings(self, world: "LingoWorld") -> bool:
self.painting_mapping.clear()
door_shuffle = world.options.shuffle_doors
# First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to
# required paintings.
req_exits = []
required_painting_rooms = REQUIRED_PAINTING_ROOMS
if door_shuffle == ShuffleDoors.option_none:
if world.options.shuffle_doors != ShuffleDoors.option_doors:
required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors]
@@ -432,7 +475,7 @@ class LingoPlayerLogic:
for painting_id, painting in PAINTINGS.items():
if painting_id not in self.painting_mapping.values() \
and (painting.required or (painting.required_when_no_doors and
door_shuffle == ShuffleDoors.option_none)):
world.options.shuffle_doors != ShuffleDoors.option_doors)):
return False
return True
@@ -447,12 +490,31 @@ class LingoPlayerLogic:
access_reqs = AccessRequirements()
panel_object = PANELS_BY_ROOM[room][panel]
if world.options.shuffle_doors == ShuffleDoors.option_panels and panel_object.panel_door is not None:
panel_door_room = panel_object.panel_door.room
panel_door_name = panel_object.panel_door.panel_door
panel_door = PANEL_DOORS_BY_ROOM[panel_door_room][panel_door_name]
if panel_door.panel_group is not None and world.options.group_doors:
access_reqs.items.add(panel_door.panel_group)
elif panel_door_room in PROGRESSIVE_PANELS_BY_ROOM\
and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[panel_door_room]:
progression_obj = PROGRESSIVE_PANELS_BY_ROOM[panel_door_room][panel_door_name]
progression_handling = should_split_progression(progression_obj.item_name, world)
if progression_handling == ProgressiveItemBehavior.SPLIT:
access_reqs.items.add(panel_door.item_name)
elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE:
access_reqs.progression[progression_obj.item_name] = progression_obj.index
else:
access_reqs.items.add(panel_door.item_name)
for req_room in panel_object.required_rooms:
access_reqs.rooms.add(req_room)
for req_door in panel_object.required_doors:
door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door]
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
if door_object.event or world.options.shuffle_doors != ShuffleDoors.option_doors:
sub_access_reqs = self.calculate_door_requirements(
room if req_door.room is None else req_door.room, req_door.door, world)
access_reqs.merge(sub_access_reqs)
@@ -522,11 +584,14 @@ class LingoPlayerLogic:
continue
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will
# only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. THE MASTER has
# special access rules and is handled separately.
# only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. Panel door locked
# puzzles will be separate if panels mode is on. THE MASTER has special access rules and is handled
# separately.
if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\
or len(panel_data.required_rooms) > 0\
or (world.options.shuffle_colors and len(panel_data.colors) > 1)\
or (world.options.shuffle_doors == ShuffleDoors.option_panels
and panel_data.panel_door is not None)\
or panel_name == "THE MASTER":
self.counting_panel_reqs.setdefault(room_name, []).append(
(self.calculate_panel_requirements(room_name, panel_name, world), 1))

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from .datatypes import RoomAndDoor
from .player_logic import AccessRequirements, PlayerLocation
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS
from .static_logic import PROGRESSIVE_DOORS_BY_ROOM, PROGRESSIVE_ITEMS
if TYPE_CHECKING:
from . import LingoWorld
@@ -59,6 +59,12 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
if not state.has(color.capitalize(), world.player):
return False
if not all(state.has(item, world.player) for item in access.items):
return False
if not all(state.has(item, world.player, index) for item, index in access.progression.items()):
return False
if access.the_master and not lingo_can_use_mastery_location(state, world):
return False
@@ -77,7 +83,7 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L
item_name = world.player_logic.item_by_door[room][door]
if item_name in PROGRESSIVE_ITEMS:
progression = PROGRESSION_BY_ROOM[room][door]
progression = PROGRESSIVE_DOORS_BY_ROOM[room][door]
return state.has(item_name, world.player, progression.index)
return state.has(item_name, world.player)

View File

@@ -4,15 +4,17 @@ import pickle
from io import BytesIO
from typing import Dict, List, Set
from .datatypes import Door, Painting, Panel, Progression, Room
from .datatypes import Door, Painting, Panel, PanelDoor, Progression, Room
ALL_ROOMS: List[Room] = []
DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {}
PAINTINGS: Dict[str, Painting] = {}
PROGRESSIVE_ITEMS: List[str] = []
PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PROGRESSIVE_ITEMS: Set[str] = set()
PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PAINTING_ENTRANCES: int = 0
PAINTING_EXIT_ROOMS: Set[str] = set()
@@ -28,6 +30,8 @@ PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
PANEL_GROUP_ITEM_IDS: Dict[str, int] = {}
PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
HASHES: Dict[str, str] = {}
@@ -68,6 +72,20 @@ def get_door_group_item_id(name: str):
return DOOR_GROUP_ITEM_IDS[name]
def get_panel_door_item_id(room: str, name: str):
if room not in PANEL_DOOR_ITEM_IDS or name not in PANEL_DOOR_ITEM_IDS[room]:
raise Exception(f"Item ID for panel door {room} - {name} not found in ids.yaml.")
return PANEL_DOOR_ITEM_IDS[room][name]
def get_panel_group_item_id(name: str):
if name not in PANEL_GROUP_ITEM_IDS:
raise Exception(f"Item ID for panel group {name} not found in ids.yaml.")
return PANEL_GROUP_ITEM_IDS[name]
def get_progressive_item_id(name: str):
if name not in PROGRESSIVE_ITEM_IDS:
raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.")
@@ -97,8 +115,10 @@ def load_static_data_from_file():
ALL_ROOMS.extend(pickdata["ALL_ROOMS"])
DOORS_BY_ROOM.update(pickdata["DOORS_BY_ROOM"])
PANELS_BY_ROOM.update(pickdata["PANELS_BY_ROOM"])
PROGRESSIVE_ITEMS.extend(pickdata["PROGRESSIVE_ITEMS"])
PROGRESSION_BY_ROOM.update(pickdata["PROGRESSION_BY_ROOM"])
PANEL_DOORS_BY_ROOM.update(pickdata["PANEL_DOORS_BY_ROOM"])
PROGRESSIVE_ITEMS.update(pickdata["PROGRESSIVE_ITEMS"])
PROGRESSIVE_DOORS_BY_ROOM.update(pickdata["PROGRESSIVE_DOORS_BY_ROOM"])
PROGRESSIVE_PANELS_BY_ROOM.update(pickdata["PROGRESSIVE_PANELS_BY_ROOM"])
PAINTING_ENTRANCES = pickdata["PAINTING_ENTRANCES"]
PAINTING_EXIT_ROOMS.update(pickdata["PAINTING_EXIT_ROOMS"])
PAINTING_EXITS = pickdata["PAINTING_EXITS"]
@@ -111,6 +131,8 @@ def load_static_data_from_file():
DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"])
DOOR_ITEM_IDS.update(pickdata["DOOR_ITEM_IDS"])
DOOR_GROUP_ITEM_IDS.update(pickdata["DOOR_GROUP_ITEM_IDS"])
PANEL_DOOR_ITEM_IDS.update(pickdata["PANEL_DOOR_ITEM_IDS"])
PANEL_GROUP_ITEM_IDS.update(pickdata["PANEL_GROUP_ITEM_IDS"])
PROGRESSIVE_ITEM_IDS.update(pickdata["PROGRESSIVE_ITEM_IDS"])

View File

@@ -3,7 +3,7 @@ from . import LingoTestBase
class TestRequiredRoomLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"shuffle_colors": "false",
}
@@ -50,7 +50,7 @@ class TestRequiredRoomLogic(LingoTestBase):
class TestRequiredDoorLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"shuffle_colors": "false",
}
@@ -78,7 +78,8 @@ class TestRequiredDoorLogic(LingoTestBase):
class TestSimpleDoors(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_doors": "doors",
"group_doors": "true",
"shuffle_colors": "false",
}
@@ -90,3 +91,52 @@ class TestSimpleDoors(LingoTestBase):
self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
class TestPanels(LingoTestBase):
options = {
"shuffle_doors": "panels"
}
def test_requirement(self):
self.assertFalse(self.can_reach_location("Starting Room - HIDDEN"))
self.assertFalse(self.can_reach_location("Hidden Room - OPEN"))
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
self.collect_by_name("Starting Room - HIDDEN (Panel)")
self.assertTrue(self.can_reach_location("Starting Room - HIDDEN"))
self.assertFalse(self.can_reach_location("Hidden Room - OPEN"))
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
self.collect_by_name("Hidden Room - OPEN (Panel)")
self.assertTrue(self.can_reach_location("Starting Room - HIDDEN"))
self.assertTrue(self.can_reach_location("Hidden Room - OPEN"))
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
class TestGroupedPanels(LingoTestBase):
options = {
"shuffle_doors": "panels",
"group_doors": "true",
"shuffle_colors": "false",
}
def test_requirement(self):
self.assertFalse(self.can_reach_location("Hub Room - SLAUGHTER"))
self.assertFalse(self.can_reach_location("Dread Hallway - DREAD"))
self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
self.collect_by_name("Tenacious Entrance Panels")
self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
self.assertFalse(self.can_reach_location("Dread Hallway - DREAD"))
self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
self.collect_by_name("Outside The Agreeable - BLACK (Panel)")
self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
self.assertTrue(self.can_reach_location("Dread Hallway - DREAD"))
self.assertFalse(self.can_reach_location("The Tenacious - Achievement"))
self.collect_by_name("The Tenacious - Black Palindromes (Panels)")
self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER"))
self.assertTrue(self.can_reach_location("Dread Hallway - DREAD"))
self.assertTrue(self.can_reach_location("The Tenacious - Achievement"))

View File

@@ -3,7 +3,7 @@ from . import LingoTestBase
class TestMultiShuffleOptions(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"progressive_orange_tower": "true",
"shuffle_colors": "true",
"shuffle_paintings": "true",
@@ -13,7 +13,7 @@ class TestMultiShuffleOptions(LingoTestBase):
class TestPanelsanity(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"progressive_orange_tower": "true",
"location_checks": "insanity",
"shuffle_colors": "true"
@@ -22,7 +22,18 @@ class TestPanelsanity(LingoTestBase):
class TestAllPanelHunt(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"progressive_orange_tower": "true",
"shuffle_colors": "true",
"victory_condition": "level_2",
"level_2_requirement": "800",
"early_color_hallways": "true"
}
class TestAllPanelHuntPanelsMode(LingoTestBase):
options = {
"shuffle_doors": "panels",
"progressive_orange_tower": "true",
"shuffle_colors": "true",
"victory_condition": "level_2",

View File

@@ -3,7 +3,7 @@ from . import LingoTestBase
class TestProgressiveOrangeTower(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"progressive_orange_tower": "true"
}

View File

@@ -3,7 +3,7 @@ from . import LingoTestBase
class TestPanelHunt(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"location_checks": "insanity",
"victory_condition": "level_2",
"level_2_requirement": "15"

View File

@@ -18,7 +18,7 @@ class TestPilgrimageWithRoofAndPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"pilgrimage_allows_roof_access": "true",
"pilgrimage_allows_paintings": "true",
"early_color_hallways": "false"
@@ -39,7 +39,7 @@ class TestPilgrimageNoRoofYesPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"pilgrimage_allows_roof_access": "false",
"pilgrimage_allows_paintings": "true",
"early_color_hallways": "false"
@@ -62,7 +62,7 @@ class TestPilgrimageNoRoofNoPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"pilgrimage_allows_roof_access": "false",
"pilgrimage_allows_paintings": "false",
"early_color_hallways": "false"
@@ -117,7 +117,7 @@ class TestPilgrimageYesRoofNoPaintings(LingoTestBase):
options = {
"enable_pilgrimage": "true",
"shuffle_colors": "false",
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"pilgrimage_allows_roof_access": "true",
"pilgrimage_allows_paintings": "false",
"early_color_hallways": "false"

View File

@@ -3,7 +3,7 @@ from . import LingoTestBase
class TestComplexProgressiveHallwayRoom(LingoTestBase):
options = {
"shuffle_doors": "complex"
"shuffle_doors": "doors"
}
def test_item(self):
@@ -54,7 +54,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase):
class TestSimpleHallwayRoom(LingoTestBase):
options = {
"shuffle_doors": "simple"
"shuffle_doors": "doors",
"group_doors": "true",
}
def test_item(self):
@@ -81,7 +82,7 @@ class TestSimpleHallwayRoom(LingoTestBase):
class TestProgressiveArtGallery(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"shuffle_colors": "false",
}

View File

@@ -19,7 +19,8 @@ class TestVanillaDoorsNormalSunwarps(LingoTestBase):
class TestSimpleDoorsNormalSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_doors": "doors",
"group_doors": "true",
"sunwarp_access": "normal"
}
@@ -37,7 +38,8 @@ class TestSimpleDoorsNormalSunwarps(LingoTestBase):
class TestSimpleDoorsDisabledSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_doors": "doors",
"group_doors": "true",
"sunwarp_access": "disabled"
}
@@ -56,7 +58,8 @@ class TestSimpleDoorsDisabledSunwarps(LingoTestBase):
class TestSimpleDoorsUnlockSunwarps(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_doors": "doors",
"group_doors": "true",
"sunwarp_access": "unlock"
}
@@ -78,7 +81,8 @@ class TestSimpleDoorsUnlockSunwarps(LingoTestBase):
class TestComplexDoorsNormalSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"group_doors": "false",
"sunwarp_access": "normal"
}
@@ -96,7 +100,8 @@ class TestComplexDoorsNormalSunwarps(LingoTestBase):
class TestComplexDoorsDisabledSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"group_doors": "false",
"sunwarp_access": "disabled"
}
@@ -115,7 +120,8 @@ class TestComplexDoorsDisabledSunwarps(LingoTestBase):
class TestComplexDoorsIndividualSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"group_doors": "false",
"sunwarp_access": "individual"
}
@@ -142,7 +148,8 @@ class TestComplexDoorsIndividualSunwarps(LingoTestBase):
class TestComplexDoorsProgressiveSunwarps(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_doors": "doors",
"group_doors": "false",
"sunwarp_access": "progressive"
}

View File

@@ -73,6 +73,22 @@ if old_generated.include? "door_groups" then
end
end
end
if old_generated.include? "panel_doors" then
old_generated["panel_doors"].each do |room, panel_doors|
panel_doors.each do |name, id|
if id >= next_item_id then
next_item_id = id + 1
end
end
end
end
if old_generated.include? "panel_groups" then
old_generated["panel_groups"].each do |name, id|
if id >= next_item_id then
next_item_id = id + 1
end
end
end
if old_generated.include? "progression" then
old_generated["progression"].each do |name, id|
if id >= next_item_id then
@@ -82,6 +98,7 @@ if old_generated.include? "progression" then
end
door_groups = Set[]
panel_groups = Set[]
config = YAML.load_file(configpath)
config.each do |room_name, room_data|
@@ -163,6 +180,29 @@ config.each do |room_name, room_data|
end
end
if room_data.include? "panel_doors"
room_data["panel_doors"].each do |panel_door_name, panel_door|
unless old_generated.include? "panel_doors" and old_generated["panel_doors"].include? room_name and old_generated["panel_doors"][room_name].include? panel_door_name then
old_generated["panel_doors"] ||= {}
old_generated["panel_doors"][room_name] ||= {}
old_generated["panel_doors"][room_name][panel_door_name] = next_item_id
next_item_id += 1
end
if panel_door.include? "panel_group" and not panel_groups.include? panel_door["panel_group"] then
panel_groups.add(panel_door["panel_group"])
unless old_generated.include? "panel_groups" and old_generated["panel_groups"].include? panel_door["panel_group"] then
old_generated["panel_groups"] ||= {}
old_generated["panel_groups"][panel_door["panel_group"]] = next_item_id
next_item_id += 1
end
end
end
end
if room_data.include? "progression"
room_data["progression"].each do |progression_name, pdata|
unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then

View File

@@ -6,8 +6,8 @@ import sys
sys.path.append(os.path.join("worlds", "lingo"))
sys.path.append(".")
sys.path.append("..")
from datatypes import Door, DoorType, EntranceType, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel,\
RoomEntrance
from datatypes import Door, DoorType, EntranceType, Painting, Panel, PanelDoor, Progression, Room, RoomAndDoor,\
RoomAndPanel, RoomAndPanelDoor, RoomEntrance
import hashlib
import pickle
@@ -18,10 +18,12 @@ import Utils
ALL_ROOMS: List[Room] = []
DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {}
PAINTINGS: Dict[str, Painting] = {}
PROGRESSIVE_ITEMS: List[str] = []
PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PROGRESSIVE_ITEMS: Set[str] = set()
PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PAINTING_ENTRANCES: int = 0
PAINTING_EXIT_ROOMS: Set[str] = set()
@@ -37,8 +39,13 @@ PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
PANEL_GROUP_ITEM_IDS: Dict[str, int] = {}
PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
# This doesn't need to be stored in the datafile.
PANEL_DOOR_BY_PANEL_BY_ROOM: Dict[str, Dict[str, str]] = {}
def hash_file(path):
md5 = hashlib.md5()
@@ -53,7 +60,7 @@ def hash_file(path):
def load_static_data(ll1_path, ids_path):
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
with open(ids_path, "r") as file:
@@ -86,6 +93,17 @@ def load_static_data(ll1_path, ids_path):
for item_name, item_id in config["door_groups"].items():
DOOR_GROUP_ITEM_IDS[item_name] = item_id
if "panel_doors" in config:
for room_name, panel_doors in config["panel_doors"].items():
PANEL_DOOR_ITEM_IDS[room_name] = {}
for panel_door, item_id in panel_doors.items():
PANEL_DOOR_ITEM_IDS[room_name][panel_door] = item_id
if "panel_groups" in config:
for item_name, item_id in config["panel_groups"].items():
PANEL_GROUP_ITEM_IDS[item_name] = item_id
if "progression" in config:
for item_name, item_id in config["progression"].items():
PROGRESSIVE_ITEM_IDS[item_name] = item_id
@@ -147,6 +165,46 @@ def process_entrance(source_room, doors, room_obj):
room_obj.entrances.append(RoomEntrance(source_room, door, entrance_type))
def process_panel_door(room_name, panel_door_name, panel_door_data):
global PANEL_DOORS_BY_ROOM, PANEL_DOOR_BY_PANEL_BY_ROOM
panels: List[RoomAndPanel] = list()
for panel in panel_door_data["panels"]:
if isinstance(panel, dict):
panels.append(RoomAndPanel(panel["room"], panel["panel"]))
else:
panels.append(RoomAndPanel(room_name, panel))
for panel in panels:
PANEL_DOOR_BY_PANEL_BY_ROOM.setdefault(panel.room, {})[panel.panel] = RoomAndPanelDoor(room_name,
panel_door_name)
if "item_name" in panel_door_data:
item_name = panel_door_data["item_name"]
else:
panel_per_room = dict()
for panel in panels:
panel_room_name = room_name if panel.room is None else panel.room
panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
room_strs = list()
for door_room_str, door_panels_str in panel_per_room.items():
room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
if len(panels) == 1:
item_name = f"{room_strs[0]} (Panel)"
else:
item_name = " and ".join(room_strs) + " (Panels)"
if "panel_group" in panel_door_data:
panel_group = panel_door_data["panel_group"]
else:
panel_group = None
panel_door_obj = PanelDoor(item_name, panel_group)
PANEL_DOORS_BY_ROOM[room_name][panel_door_name] = panel_door_obj
def process_panel(room_name, panel_name, panel_data):
global PANELS_BY_ROOM
@@ -227,13 +285,18 @@ def process_panel(room_name, panel_name, panel_data):
else:
non_counting = False
if room_name in PANEL_DOOR_BY_PANEL_BY_ROOM and panel_name in PANEL_DOOR_BY_PANEL_BY_ROOM[room_name]:
panel_door = PANEL_DOOR_BY_PANEL_BY_ROOM[room_name][panel_name]
else:
panel_door = None
if "location_name" in panel_data:
location_name = panel_data["location_name"]
else:
location_name = None
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce,
achievement, non_counting, location_name)
achievement, non_counting, panel_door, location_name)
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
@@ -325,7 +388,7 @@ def process_door(room_name, door_name, door_data):
painting_ids = []
door_type = DoorType.NORMAL
if door_name.endswith(" Sunwarp"):
if room_name == "Sunwarps":
door_type = DoorType.SUNWARP
elif room_name == "Pilgrim Antechamber" and door_name == "Sun Painting":
door_type = DoorType.SUN_PAINTING
@@ -404,11 +467,11 @@ def process_sunwarp(room_name, sunwarp_data):
SUNWARP_EXITS[sunwarp_data["dots"] - 1] = room_name
def process_progression(room_name, progression_name, progression_doors):
global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
def process_progressive_door(room_name, progression_name, progression_doors):
global PROGRESSIVE_ITEMS, PROGRESSIVE_DOORS_BY_ROOM
# Progressive items are configured as a list of doors.
PROGRESSIVE_ITEMS.append(progression_name)
PROGRESSIVE_ITEMS.add(progression_name)
progression_index = 1
for door in progression_doors:
@@ -419,11 +482,31 @@ def process_progression(room_name, progression_name, progression_doors):
door_room = room_name
door_door = door
room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {})
room_progressions = PROGRESSIVE_DOORS_BY_ROOM.setdefault(door_room, {})
room_progressions[door_door] = Progression(progression_name, progression_index)
progression_index += 1
def process_progressive_panel(room_name, progression_name, progression_panel_doors):
global PROGRESSIVE_ITEMS, PROGRESSIVE_PANELS_BY_ROOM
# Progressive items are configured as a list of panel doors.
PROGRESSIVE_ITEMS.add(progression_name)
progression_index = 1
for panel_door in progression_panel_doors:
if isinstance(panel_door, Dict):
panel_door_room = panel_door["room"]
panel_door_door = panel_door["panel_door"]
else:
panel_door_room = room_name
panel_door_door = panel_door
room_progressions = PROGRESSIVE_PANELS_BY_ROOM.setdefault(panel_door_room, {})
room_progressions[panel_door_door] = Progression(progression_name, progression_index)
progression_index += 1
def process_room(room_name, room_data):
global ALL_ROOMS
@@ -433,6 +516,12 @@ def process_room(room_name, room_data):
for source_room, doors in room_data["entrances"].items():
process_entrance(source_room, doors, room_obj)
if "panel_doors" in room_data:
PANEL_DOORS_BY_ROOM[room_name] = dict()
for panel_door_name, panel_door_data in room_data["panel_doors"].items():
process_panel_door(room_name, panel_door_name, panel_door_data)
if "panels" in room_data:
PANELS_BY_ROOM[room_name] = dict()
@@ -454,8 +543,11 @@ def process_room(room_name, room_data):
process_sunwarp(room_name, sunwarp_data)
if "progression" in room_data:
for progression_name, progression_doors in room_data["progression"].items():
process_progression(room_name, progression_name, progression_doors)
for progression_name, pdata in room_data["progression"].items():
if "doors" in pdata:
process_progressive_door(room_name, progression_name, pdata["doors"])
if "panel_doors" in pdata:
process_progressive_panel(room_name, progression_name, pdata["panel_doors"])
ALL_ROOMS.append(room_obj)
@@ -492,8 +584,10 @@ if __name__ == '__main__':
"ALL_ROOMS": ALL_ROOMS,
"DOORS_BY_ROOM": DOORS_BY_ROOM,
"PANELS_BY_ROOM": PANELS_BY_ROOM,
"PANEL_DOORS_BY_ROOM": PANEL_DOORS_BY_ROOM,
"PROGRESSIVE_ITEMS": PROGRESSIVE_ITEMS,
"PROGRESSION_BY_ROOM": PROGRESSION_BY_ROOM,
"PROGRESSIVE_DOORS_BY_ROOM": PROGRESSIVE_DOORS_BY_ROOM,
"PROGRESSIVE_PANELS_BY_ROOM": PROGRESSIVE_PANELS_BY_ROOM,
"PAINTING_ENTRANCES": PAINTING_ENTRANCES,
"PAINTING_EXIT_ROOMS": PAINTING_EXIT_ROOMS,
"PAINTING_EXITS": PAINTING_EXITS,
@@ -506,6 +600,8 @@ if __name__ == '__main__':
"DOOR_LOCATION_IDS": DOOR_LOCATION_IDS,
"DOOR_ITEM_IDS": DOOR_ITEM_IDS,
"DOOR_GROUP_ITEM_IDS": DOOR_GROUP_ITEM_IDS,
"PANEL_DOOR_ITEM_IDS": PANEL_DOOR_ITEM_IDS,
"PANEL_GROUP_ITEM_IDS": PANEL_GROUP_ITEM_IDS,
"PROGRESSIVE_ITEM_IDS": PROGRESSIVE_ITEM_IDS,
}

View File

@@ -33,19 +33,23 @@ end
configured_rooms = Set["Menu"]
configured_doors = Set[]
configured_panels = Set[]
configured_panel_doors = Set[]
mentioned_rooms = Set[]
mentioned_doors = Set[]
mentioned_panels = Set[]
mentioned_panel_doors = Set[]
mentioned_sunwarp_entrances = Set[]
mentioned_sunwarp_exits = Set[]
mentioned_paintings = Set[]
door_groups = {}
panel_groups = {}
directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"]
directives = Set["entrances", "panels", "doors", "panel_doors", "paintings", "sunwarps", "progression"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"]
door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"]
panel_door_directives = Set["panels", "item_name", "panel_group"]
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]
non_counting = 0
@@ -253,6 +257,43 @@ config.each do |room_name, room|
end
end
(room["panel_doors"] || {}).each do |panel_door_name, panel_door|
configured_panel_doors.add("#{room_name} - #{panel_door_name}")
if panel_door.include?("panels")
panel_door["panels"].each do |panel|
if panel.kind_of? Hash then
other_room = panel.include?("room") ? panel["room"] : room_name
mentioned_panels.add("#{other_room} - #{panel["panel"]}")
else
other_room = panel.include?("room") ? panel["room"] : room_name
mentioned_panels.add("#{room_name} - #{panel}")
end
end
else
puts "#{room_name} - #{panel_door_name} :::: Missing panels field"
end
if panel_door.include?("panel_group")
panel_groups[panel_door["panel_group"]] ||= 0
panel_groups[panel_door["panel_group"]] += 1
end
bad_subdirectives = []
panel_door.keys.each do |key|
unless panel_door_directives.include?(key) then
bad_subdirectives << key
end
end
unless bad_subdirectives.empty? then
puts "#{room_name} - #{panel_door_name} :::: Panel door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
end
unless ids.include?("panel_doors") and ids["panel_doors"].include?(room_name) and ids["panel_doors"][room_name].include?(panel_door_name)
puts "#{room_name} - #{panel_door_name} :::: Panel door is missing an item ID"
end
end
(room["paintings"] || []).each do |painting|
if painting.include?("id") and painting["id"].kind_of? String then
unless paintings.include? painting["id"] then
@@ -327,12 +368,24 @@ config.each do |room_name, room|
end
end
(room["progression"] || {}).each do |progression_name, door_list|
door_list.each do |door|
if door.kind_of? Hash then
mentioned_doors.add("#{door["room"]} - #{door["door"]}")
else
mentioned_doors.add("#{room_name} - #{door}")
(room["progression"] || {}).each do |progression_name, pdata|
if pdata.include? "doors" then
pdata["doors"].each do |door|
if door.kind_of? Hash then
mentioned_doors.add("#{door["room"]} - #{door["door"]}")
else
mentioned_doors.add("#{room_name} - #{door}")
end
end
end
if pdata.include? "panel_doors" then
pdata["panel_doors"].each do |panel_door|
if panel_door.kind_of? Hash then
mentioned_panel_doors.add("#{panel_door["room"]} - #{panel_door["panel_door"]}")
else
mentioned_panel_doors.add("#{room_name} - #{panel_door}")
end
end
end
@@ -344,17 +397,22 @@ end
errored_rooms = mentioned_rooms - configured_rooms
unless errored_rooms.empty? then
puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s
puts "The following rooms are mentioned but do not exist: " + errored_rooms.to_s
end
errored_panels = mentioned_panels - configured_panels
unless errored_panels.empty? then
puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s
puts "The following panels are mentioned but do not exist: " + errored_panels.to_s
end
errored_doors = mentioned_doors - configured_doors
unless errored_doors.empty? then
puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s
puts "The following doors are mentioned but do not exist: " + errored_doors.to_s
end
errored_panel_doors = mentioned_panel_doors - configured_panel_doors
unless errored_panel_doors.empty? then
puts "The following panel doors are mentioned but do not exist: " + errored_panel_doors.to_s
end
door_groups.each do |group,num|
@@ -367,6 +425,16 @@ door_groups.each do |group,num|
end
end
panel_groups.each do |group,num|
if num == 1 then
puts "Panel group \"#{group}\" only has one panel in it"
end
unless ids.include?("panel_groups") and ids["panel_groups"].include?(group)
puts "#{group} :::: Panel group is missing an item ID"
end
end
slashed_rooms = configured_rooms.select do |room|
room.include? "/"
end

View File

@@ -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):

View File

@@ -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.

View File

@@ -427,7 +427,7 @@ location_data = [
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)),

View File

@@ -215,7 +215,6 @@ class SMZ3World(World):
niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World)
junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World)
allJunkItems = niceItems + junkItems
self.junkItemsNames = [item.Type.name for item in junkItems]
if (self.smz3World.Config.Keysanity):
@@ -228,7 +227,8 @@ class SMZ3World(World):
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \
[SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems]
[SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \
[SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems]
self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon]
self.multiworld.itempool += itemPool

View File

@@ -1,4 +1,5 @@
import logging
from random import Random
from typing import Dict, Any, Iterable, Optional, Union, List, TextIO
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
@@ -27,15 +28,20 @@ from .strings.goal_names import Goal as GoalName
from .strings.metal_names import Ore
from .strings.region_names import Region as RegionName, LogicRegion
logger = logging.getLogger(__name__)
STARDEW_VALLEY = "Stardew Valley"
UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed"
client_version = 0
class StardewLocation(Location):
game: str = "Stardew Valley"
game: str = STARDEW_VALLEY
class StardewItem(Item):
game: str = "Stardew Valley"
game: str = STARDEW_VALLEY
class StardewWebWorld(WebWorld):
@@ -60,7 +66,7 @@ class StardewValleyWorld(World):
Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests,
befriend villagers, and uncover dark secrets.
"""
game = "Stardew Valley"
game = STARDEW_VALLEY
topology_present = False
item_name_to_id = {name: data.code for name, data in item_table.items()}
@@ -95,6 +101,17 @@ class StardewValleyWorld(World):
self.total_progression_items = 0
# self.all_progression_items = dict()
# Taking the seed specified in slot data for UT, otherwise just generating the seed.
self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64))
self.random = Random(self.seed)
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]:
# If the seed is not specified in the slot data, this mean the world was generated before Universal Tracker support.
seed = slot_data.get(UNIVERSAL_TRACKER_SEED_PROPERTY)
if seed is None:
logger.warning(f"World was generated before Universal Tracker support. Tracker might not be accurate.")
return seed
def generate_early(self):
self.force_change_options_if_incompatible()
self.content = create_content(self.options)
@@ -108,12 +125,12 @@ class StardewValleyWorld(World):
self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false
goal_name = self.options.goal.current_key
player_name = self.multiworld.player_name[self.player]
logging.warning(
logger.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(
logger.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):
@@ -413,6 +430,7 @@ class StardewValleyWorld(World):
included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names]
slot_data = self.options.as_dict(*included_option_names)
slot_data.update({
UNIVERSAL_TRACKER_SEED_PROPERTY: self.seed,
"seed": self.random.randrange(1000000000), # Seed should be max 9 digits
"randomized_entrances": self.randomized_entrances,
"modified_bundles": bundles,

View File

@@ -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,
)
))

View File

@@ -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),),
))

View File

@@ -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 @@ register_mod_content_pack(ContentPack(
),
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)),)
}
))

View File

@@ -73,13 +73,6 @@ register_mod_content_pack(ContentPack(
)
))
register_mod_content_pack(ContentPack(
ModNames.alecto,
villagers=(
villagers_data.alecto,
)
))
register_mod_content_pack(ContentPack(
ModNames.lacey,
villagers=(

View File

@@ -3,15 +3,27 @@ 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 ...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 @@ class SVEContentPack(ContentPack):
# 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 @@ register_mod_content_pack(SVEContentPack(
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 @@ register_mod_content_pack(SVEContentPack(
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

View File

@@ -229,7 +229,7 @@ pelican_town = ContentPack(
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 @@ pelican_town = ContentPack(
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))),

View File

@@ -46,7 +46,8 @@ pirate_cove = (Region.pirate_cove,)
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 @@ bonefish = create_fish(SVEFish.bonefish, crimson_badlands, season.all_seasons, 7
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)

View File

@@ -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,
1 id name classification groups mod_name
307 322 Phoenix Ring useful RING,RESOURCE_PACK,MAXIMUM_ONE
308 323 Immunity Band useful RING,RESOURCE_PACK,MAXIMUM_ONE
309 324 Glowstone Ring useful RING,RESOURCE_PACK,MAXIMUM_ONE
310 325 Fairy Dust Recipe progression GINGER_ISLAND
311 326 Heavy Tapper Recipe progression QI_CRAFTING_RECIPE,GINGER_ISLAND
312 327 Hyper Speed-Gro Recipe progression QI_CRAFTING_RECIPE,GINGER_ISLAND
313 328 Deluxe Fertilizer Recipe progression QI_CRAFTING_RECIPE

View File

@@ -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,
@@ -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
1 id region name tags mod_name
2088 3472 Farm Craft Life Elixir CRAFTSANITY
2089 3473 Farm Craft Oil of Garlic CRAFTSANITY
2090 3474 Farm Craft Monster Musk CRAFTSANITY
2091 3475 Farm Craft Fairy Dust CRAFTSANITY CRAFTSANITY,GINGER_ISLAND
2092 3476 Farm Craft Warp Totem: Beach CRAFTSANITY
2093 3477 Farm Craft Warp Totem: Mountains CRAFTSANITY
2094 3478 Farm Craft Warp Totem: Farm CRAFTSANITY
2900 7055 Abandoned Mines - 3 Abandoned Treasure - Floor 3 MANDATORY Boarding House and Bus Stop Extension
2901 7056 Abandoned Mines - 4 Abandoned Treasure - Floor 4 MANDATORY Boarding House and Bus Stop Extension
2902 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
2903 7401 Farm Cook Magic Elixir COOKSANITY Magic
2904 7402 Farm Craft Travel Core CRAFTSANITY Magic
2905 7403 Farm Craft Haste Elixir CRAFTSANITY Stardew Valley Expanded
3279 8237 Shipping Shipsanity: Pterodactyl R Wing Bone SHIPSANITY Boarding House and Bus Stop Extension
3280 8238 Shipping Shipsanity: Scrap Rust SHIPSANITY Archaeology
3281 8239 Shipping Shipsanity: Rusty Path SHIPSANITY Archaeology
8240 Shipping Shipsanity: Digging Like Worms SHIPSANITY Archaeology
3282 8241 Shipping Shipsanity: Digger's Delight SHIPSANITY Archaeology
3283 8242 Shipping Shipsanity: Rocky Root Coffee SHIPSANITY Archaeology
3284 8243 Shipping Shipsanity: Ancient Jello SHIPSANITY Archaeology
3285 8244 Shipping Shipsanity: Bone Fence SHIPSANITY Archaeology
3286 8245 Shipping Shipsanity: Grilled Cheese SHIPSANITY Binning Skill
3287 8246 Shipping Shipsanity: Fish Casserole SHIPSANITY Binning Skill
3288 8247 Shipping Shipsanity: Snatcher Worm SHIPSANITY Stardew Valley Expanded

View File

@@ -5,7 +5,7 @@ from ..strings.animal_product_names import AnimalProduct
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 @@ mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fru
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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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]):

View File

@@ -3,15 +3,20 @@ from typing import Union, Iterable
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 @@ class RequirementLogicMixin(BaseLogicMixin):
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 @@ SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]):
@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)

View File

@@ -23,24 +23,15 @@ from ...logic.tool_logic import ToolLogicMixin
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 @@ FarmingLogicMixin]]):
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 @@ FarmingLogicMixin]]):
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 @@ FarmingLogicMixin]]):
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 @@ FarmingLogicMixin]]):
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 @@ FarmingLogicMixin]]):
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)

View File

@@ -183,7 +183,8 @@ stardew_valley_expanded_regions = [
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 @@ mandatory_sve_connections = [
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 = [

View File

@@ -137,7 +137,8 @@ vanilla_regions = [
[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],
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),
@@ -536,7 +537,7 @@ def create_final_regions(world_options) -> List[RegionData]:
def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]:
regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)}
connections = {connection.name: connection for connection in vanilla_connections}
connections = modify_connections_for_mods(connections, world_options.mods)
connections = modify_connections_for_mods(connections, sorted(world_options.mods.value))
include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false
return remove_ginger_island_regions_and_connections(regions_data, connections, include_island)
@@ -563,10 +564,8 @@ def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, Regi
return connections, regions_by_name
def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> Dict[str, ConnectionData]:
if mods is None:
return connections
for mod in mods.value:
def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]:
for mod in mods:
if mod not in ModDataList:
continue
if mod in vanilla_connections_to_remove_by_mod:

View File

@@ -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):

View File

@@ -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()

View File

@@ -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:

View File

@@ -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"

View File

@@ -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:

View File

@@ -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:

View File

@@ -441,6 +441,16 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -
for i in range(1, len(test_options) + 1):
multiworld.game[i] = StardewValleyWorld.game
multiworld.player_name.update({i: f"Tester{i}"})
args = create_args(test_options)
multiworld.set_options(args)
for step in gen_steps:
call_all(multiworld, step)
return multiworld
def create_args(test_options):
args = Namespace()
for name, option in StardewValleyWorld.options_dataclass.type_hints.items():
options = {}
@@ -449,9 +459,4 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -
value = option(player_options[name]) if name in player_options else option.from_any(option.default)
options.update({i: value})
setattr(args, name, options)
multiworld.set_options(args)
for step in gen_steps:
call_all(multiworld, step)
return multiworld
return args

View File

@@ -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 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase):
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)

View File

@@ -37,7 +37,7 @@ class TestRaccoonBundlesLogic(SVTestBase):
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
seed = 2 # 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

View File

@@ -1,6 +1,7 @@
import argparse
import json
from ...options import FarmType, EntranceRandomization
from ...test import setup_solo_multiworld, allsanity_mods_6_x_x
if __name__ == "__main__":
@@ -10,21 +11,23 @@ if __name__ == "__main__":
args = parser.parse_args()
seed = args.seed
multi_world = setup_solo_multiworld(
allsanity_mods_6_x_x(),
seed=seed
)
options = allsanity_mods_6_x_x()
options[FarmType.internal_name] = FarmType.option_standard
options[EntranceRandomization.internal_name] = EntranceRandomization.option_buildings
multi_world = setup_solo_multiworld(options, seed=seed)
world = multi_world.worlds[1]
output = {
"bundles": {
bundle_room.name: {
bundle.name: str(bundle.items)
for bundle in bundle_room.bundles
}
for bundle_room in multi_world.worlds[1].modified_bundles
for bundle_room in world.modified_bundles
},
"items": [item.name for item in multi_world.get_items()],
"location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)}
"location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)},
"slot_data": world.fill_slot_data()
}
print(json.dumps(output))

View File

@@ -24,8 +24,7 @@ class TestGenerationIsStable(SVTestCase):
if self.skip_long_tests:
raise unittest.SkipTest("Long tests disabled")
# seed = get_seed(33778671150797368040) # troubleshooting seed
seed = get_seed(74716545478307145559)
seed = get_seed()
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)])
@@ -54,3 +53,6 @@ class TestGenerationIsStable(SVTestCase):
# We check that the actual rule has the same order to make sure it is evaluated in the same order,
# so performance tests are repeatable as much as possible.
self.assertEqual(rule_a, rule_b, f"Location rule of {location_a} at index {i} is different between both executions. Seed={seed}")
for key, value in result_a["slot_data"].items():
self.assertEqual(value, result_b["slot_data"][key], f"Slot data {key} is different between both executions. Seed={seed}")

View File

@@ -0,0 +1,52 @@
import unittest
from unittest.mock import Mock
from .. import SVTestBase, create_args, allsanity_mods_6_x_x
from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization
class TestUniversalTrackerGenerationIsStable(SVTestBase):
options = allsanity_mods_6_x_x()
options.update({
EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
BundleRandomization.internal_name: BundleRandomization.option_shuffled,
FarmType.internal_name: FarmType.option_standard, # Need to choose one otherwise it's random
})
def test_all_locations_and_items_are_the_same_between_two_generations(self):
# This might open a kivy window temporarily, but it's the only way to test this...
if self.skip_long_tests:
raise unittest.SkipTest("Long tests disabled")
try:
# This test only run if UT is present, so no risk of running in the CI.
from worlds.tracker.TrackerClient import TrackerGameContext # noqa
except ImportError:
raise unittest.SkipTest("UT not loaded, skipping test")
slot_data = self.world.fill_slot_data()
ut_data = self.world.interpret_slot_data(slot_data)
fake_context = Mock()
fake_context.re_gen_passthrough = {STARDEW_VALLEY: ut_data}
args = create_args({0: self.options})
args.outputpath = None
args.outputname = None
args.multi = 1
args.race = None
args.plando_options = self.multiworld.plando_options
args.plando_items = self.multiworld.plando_items
args.plando_texts = self.multiworld.plando_texts
args.plando_connections = self.multiworld.plando_connections
args.game = self.multiworld.game
args.name = self.multiworld.player_name
args.sprite = {}
args.sprite_pool = {}
args.skip_output = True
generated_multi_world = TrackerGameContext.TMain(fake_context, args, self.multiworld.seed)
generated_slot_data = generated_multi_world.worlds[1].fill_slot_data()
# Just checking slot data should prove that UT generates the same result as AP generation.
self.maxDiff = None
self.assertEqual(slot_data, generated_slot_data)

View File

@@ -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)

View File

@@ -132,7 +132,7 @@ class TunicWorld(World):
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=multiworld.plando_connections[tunic.player])
plando=tunic.options.plando_connections)
continue
# off is more restrictive
@@ -151,9 +151,9 @@ class TunicWorld(World):
if tunic.options.fixed_shop:
cls.seed_groups[group]["fixed_shop"] = True
if multiworld.plando_connections[tunic.player]:
if tunic.options.plando_connections:
# loop through the connections in the player's yaml
for cxn in multiworld.plando_connections[tunic.player]:
for cxn in tunic.options.plando_connections:
new_cxn = True
for group_cxn in cls.seed_groups[group]["plando"]:
# if neither entrance nor exit match anything in the group, add to group