mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 22:33:21 -07:00
Merge branch 'main' into tunc-combat-logic
This commit is contained in:
@@ -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
13
Main.py
@@ -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:
|
||||
|
||||
@@ -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
45
docs/apworld_dev_faq.md
Normal 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))]
|
||||
```
|
||||
1
kvui.py
1
kvui.py
@@ -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:
|
||||
|
||||
18
settings.py
18
settings.py
@@ -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] = []
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,7 @@ from . import LingoTestBase
|
||||
|
||||
class TestProgressiveOrangeTower(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"shuffle_doors": "doors",
|
||||
"progressive_orange_tower": "true"
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
78
worlds/pokemon_emerald/docs/setup_sv.md
Normal file
78
worlds/pokemon_emerald/docs/setup_sv.md
Normal 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.
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
33
worlds/stardew_valley/content/mods/alecto.py
Normal file
33
worlds/stardew_valley/content/mods/alecto.py
Normal 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,
|
||||
)
|
||||
|
||||
))
|
||||
@@ -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),),
|
||||
|
||||
))
|
||||
|
||||
@@ -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)),)
|
||||
}
|
||||
))
|
||||
|
||||
@@ -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=(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}")
|
||||
|
||||
52
worlds/stardew_valley/test/stability/TestUniversalTracker.py
Normal file
52
worlds/stardew_valley/test/stability/TestUniversalTracker.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user