mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-24 01:51:54 -07:00
Merge branch 'main' into logic_bug_fixes
This commit is contained in:
+1
-1
@@ -150,7 +150,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.code-workspace
|
||||
*.code-workspace
|
||||
shell.nix
|
||||
|
||||
# Spyder project settings
|
||||
|
||||
@@ -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()
|
||||
|
||||
+11
-3
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# APWorld Dev FAQ
|
||||
|
||||
This document is meant as a reference tool to show solutions to common problems when developing an apworld.
|
||||
|
||||
---
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
Hint to the Generator that an item needs to be in sphere one with local_early_items
|
||||
```py
|
||||
early_item_name = "Sword"
|
||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
||||
|
||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
||||
|
||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
||||
|
||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||
```py
|
||||
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
||||
item_pool = self.create_non_filler_items()
|
||||
|
||||
while len(item_pool) < total_locations:
|
||||
item_pool.append(self.create_filler())
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
+13
-5
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ from .Locations import DLCQuestLocation, location_table
|
||||
from .Options import DLCQuestOptions
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from .presets import dlcq_options_presets
|
||||
from .option_groups import dlcq_option_groups
|
||||
|
||||
client_version = 0
|
||||
|
||||
|
||||
class DLCqwebworld(WebWorld):
|
||||
options_presets = dlcq_options_presets
|
||||
option_groups = dlcq_option_groups
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Archipelago DLCQuest game on your computer.",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import List
|
||||
|
||||
from Options import ProgressionBalancing, Accessibility, OptionGroup
|
||||
from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity,
|
||||
CoinSanityRange, DeathLink)
|
||||
|
||||
dlcq_option_groups: List[OptionGroup] = [
|
||||
OptionGroup("General", [
|
||||
Campaign,
|
||||
ItemShuffle,
|
||||
CoinSanity,
|
||||
]),
|
||||
OptionGroup("Customization", [
|
||||
EndingChoice,
|
||||
PermanentCoins,
|
||||
CoinSanityRange,
|
||||
]),
|
||||
OptionGroup("Tedious and Grind", [
|
||||
TimeIsMoney,
|
||||
DoubleJumpGlitch,
|
||||
]),
|
||||
OptionGroup("Advanced Options", [
|
||||
DeathLink,
|
||||
ProgressionBalancing,
|
||||
Accessibility,
|
||||
]),
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle
|
||||
|
||||
all_random_settings = {
|
||||
DoubleJumpGlitch.internal_name: "random",
|
||||
CoinSanity.internal_name: "random",
|
||||
CoinSanityRange.internal_name: "random",
|
||||
PermanentCoins.internal_name: "random",
|
||||
TimeIsMoney.internal_name: "random",
|
||||
EndingChoice.internal_name: "random",
|
||||
Campaign.internal_name: "random",
|
||||
ItemShuffle.internal_name: "random",
|
||||
"death_link": "random",
|
||||
}
|
||||
|
||||
main_campaign_settings = {
|
||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
||||
CoinSanityRange.internal_name: 30,
|
||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
||||
EndingChoice.internal_name: EndingChoice.option_true,
|
||||
Campaign.internal_name: Campaign.option_basic,
|
||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
||||
}
|
||||
|
||||
lfod_campaign_settings = {
|
||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
||||
CoinSanityRange.internal_name: 30,
|
||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
||||
EndingChoice.internal_name: EndingChoice.option_true,
|
||||
Campaign.internal_name: Campaign.option_live_freemium_or_die,
|
||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
||||
}
|
||||
|
||||
easy_settings = {
|
||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none,
|
||||
CoinSanity.internal_name: CoinSanity.option_none,
|
||||
CoinSanityRange.internal_name: 40,
|
||||
PermanentCoins.internal_name: PermanentCoins.option_true,
|
||||
TimeIsMoney.internal_name: TimeIsMoney.option_required,
|
||||
EndingChoice.internal_name: EndingChoice.option_true,
|
||||
Campaign.internal_name: Campaign.option_both,
|
||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
||||
}
|
||||
|
||||
hard_settings = {
|
||||
DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple,
|
||||
CoinSanity.internal_name: CoinSanity.option_coin,
|
||||
CoinSanityRange.internal_name: 30,
|
||||
PermanentCoins.internal_name: PermanentCoins.option_false,
|
||||
TimeIsMoney.internal_name: TimeIsMoney.option_optional,
|
||||
EndingChoice.internal_name: EndingChoice.option_true,
|
||||
Campaign.internal_name: Campaign.option_both,
|
||||
ItemShuffle.internal_name: ItemShuffle.option_shuffled,
|
||||
}
|
||||
|
||||
|
||||
dlcq_options_presets: Dict[str, Dict[str, Any]] = {
|
||||
"All random": all_random_settings,
|
||||
"Main campaign": main_campaign_settings,
|
||||
"LFOD campaign": lfod_campaign_settings,
|
||||
"Both easy": easy_settings,
|
||||
"Both hard": hard_settings,
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
|
||||
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
|
||||
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
|
||||
check_2 = await snes_read(ctx, 0xF53749, 1)
|
||||
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
|
||||
if check_1 != b'01' or check_2 != b'01':
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
|
||||
+10
-10
@@ -222,10 +222,10 @@ for item, data in item_table.items():
|
||||
|
||||
def create_items(self) -> None:
|
||||
items = []
|
||||
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
|
||||
starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ")
|
||||
self.multiworld.push_precollected(self.create_item(starting_weapon))
|
||||
self.multiworld.push_precollected(self.create_item("Steel Armor"))
|
||||
if self.multiworld.sky_coin_mode[self.player] == "start_with":
|
||||
if self.options.sky_coin_mode == "start_with":
|
||||
self.multiworld.push_precollected(self.create_item("Sky Coin"))
|
||||
|
||||
precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]}
|
||||
@@ -233,28 +233,28 @@ def create_items(self) -> None:
|
||||
def add_item(item_name):
|
||||
if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name:
|
||||
return
|
||||
if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key:
|
||||
if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key:
|
||||
return
|
||||
if self.multiworld.progressive_gear[self.player]:
|
||||
if self.options.progressive_gear:
|
||||
for item_group in prog_map:
|
||||
if item_name in self.item_name_groups[item_group]:
|
||||
item_name = prog_map[item_group]
|
||||
break
|
||||
if item_name == "Sky Coin":
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
||||
for _ in range(40):
|
||||
items.append(self.create_item("Sky Fragment"))
|
||||
return
|
||||
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||
elif self.options.sky_coin_mode == "save_the_crystals":
|
||||
items.append(self.create_filler())
|
||||
return
|
||||
if item_name in precollected_item_names:
|
||||
items.append(self.create_filler())
|
||||
return
|
||||
i = self.create_item(item_name)
|
||||
if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"):
|
||||
if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"):
|
||||
i.classification = ItemClassification.useful
|
||||
if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and
|
||||
if (self.options.logic == "expert" and self.options.map_shuffle == "none" and
|
||||
item_name == "Exit Book"):
|
||||
i.classification = ItemClassification.progression
|
||||
items.append(i)
|
||||
@@ -263,11 +263,11 @@ def create_items(self) -> None:
|
||||
for item in self.item_name_groups[item_group]:
|
||||
add_item(item)
|
||||
|
||||
if self.multiworld.brown_boxes[self.player] == "include":
|
||||
if self.options.brown_boxes == "include":
|
||||
filler_items = []
|
||||
for item, count in fillers.items():
|
||||
filler_items += [self.create_item(item) for _ in range(count)]
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
||||
self.multiworld.random.shuffle(filler_items)
|
||||
filler_items = filler_items[39:]
|
||||
items += filler_items
|
||||
|
||||
+35
-34
@@ -1,4 +1,5 @@
|
||||
from Options import Choice, FreeText, Toggle, Range
|
||||
from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -321,36 +322,36 @@ class KaelisMomFightsMinotaur(Toggle):
|
||||
default = 0
|
||||
|
||||
|
||||
option_definitions = {
|
||||
"logic": Logic,
|
||||
"brown_boxes": BrownBoxes,
|
||||
"sky_coin_mode": SkyCoinMode,
|
||||
"shattered_sky_coin_quantity": ShatteredSkyCoinQuantity,
|
||||
"starting_weapon": StartingWeapon,
|
||||
"progressive_gear": ProgressiveGear,
|
||||
"leveling_curve": LevelingCurve,
|
||||
"starting_companion": StartingCompanion,
|
||||
"available_companions": AvailableCompanions,
|
||||
"companions_locations": CompanionsLocations,
|
||||
"kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur,
|
||||
"companion_leveling_type": CompanionLevelingType,
|
||||
"companion_spellbook_type": CompanionSpellbookType,
|
||||
"enemies_density": EnemiesDensity,
|
||||
"enemies_scaling_lower": EnemiesScalingLower,
|
||||
"enemies_scaling_upper": EnemiesScalingUpper,
|
||||
"bosses_scaling_lower": BossesScalingLower,
|
||||
"bosses_scaling_upper": BossesScalingUpper,
|
||||
"enemizer_attacks": EnemizerAttacks,
|
||||
"enemizer_groups": EnemizerGroups,
|
||||
"shuffle_res_weak_types": ShuffleResWeakType,
|
||||
"shuffle_enemies_position": ShuffleEnemiesPositions,
|
||||
"progressive_formations": ProgressiveFormations,
|
||||
"doom_castle_mode": DoomCastle,
|
||||
"doom_castle_shortcut": DoomCastleShortcut,
|
||||
"tweak_frustrating_dungeons": TweakFrustratingDungeons,
|
||||
"map_shuffle": MapShuffle,
|
||||
"crest_shuffle": CrestShuffle,
|
||||
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
|
||||
"map_shuffle_seed": MapShuffleSeed,
|
||||
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
|
||||
}
|
||||
@dataclass
|
||||
class FFMQOptions(PerGameCommonOptions):
|
||||
logic: Logic
|
||||
brown_boxes: BrownBoxes
|
||||
sky_coin_mode: SkyCoinMode
|
||||
shattered_sky_coin_quantity: ShatteredSkyCoinQuantity
|
||||
starting_weapon: StartingWeapon
|
||||
progressive_gear: ProgressiveGear
|
||||
leveling_curve: LevelingCurve
|
||||
starting_companion: StartingCompanion
|
||||
available_companions: AvailableCompanions
|
||||
companions_locations: CompanionsLocations
|
||||
kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur
|
||||
companion_leveling_type: CompanionLevelingType
|
||||
companion_spellbook_type: CompanionSpellbookType
|
||||
enemies_density: EnemiesDensity
|
||||
enemies_scaling_lower: EnemiesScalingLower
|
||||
enemies_scaling_upper: EnemiesScalingUpper
|
||||
bosses_scaling_lower: BossesScalingLower
|
||||
bosses_scaling_upper: BossesScalingUpper
|
||||
enemizer_attacks: EnemizerAttacks
|
||||
enemizer_groups: EnemizerGroups
|
||||
shuffle_res_weak_types: ShuffleResWeakType
|
||||
shuffle_enemies_position: ShuffleEnemiesPositions
|
||||
progressive_formations: ProgressiveFormations
|
||||
doom_castle_mode: DoomCastle
|
||||
doom_castle_shortcut: DoomCastleShortcut
|
||||
tweak_frustrating_dungeons: TweakFrustratingDungeons
|
||||
map_shuffle: MapShuffle
|
||||
crest_shuffle: CrestShuffle
|
||||
shuffle_battlefield_rewards: ShuffleBattlefieldRewards
|
||||
map_shuffle_seed: MapShuffleSeed
|
||||
battlefields_battles_quantities: BattlefieldsBattlesQuantities
|
||||
|
||||
+37
-37
@@ -1,13 +1,13 @@
|
||||
import yaml
|
||||
import os
|
||||
import zipfile
|
||||
import Utils
|
||||
from copy import deepcopy
|
||||
from .Regions import object_id_table
|
||||
from Utils import __version__
|
||||
from worlds.Files import APPatch
|
||||
import pkgutil
|
||||
|
||||
settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader)
|
||||
settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml"))
|
||||
|
||||
|
||||
def generate_output(self, output_directory):
|
||||
@@ -21,7 +21,7 @@ def generate_output(self, output_directory):
|
||||
item_name = "".join(item_name.split(" "))
|
||||
else:
|
||||
if item.advancement or item.useful or (item.trap and
|
||||
self.multiworld.per_slot_randoms[self.player].randint(0, 1)):
|
||||
self.random.randint(0, 1)):
|
||||
item_name = "APItem"
|
||||
else:
|
||||
item_name = "APItemFiller"
|
||||
@@ -46,60 +46,60 @@ def generate_output(self, output_directory):
|
||||
options = deepcopy(settings_template)
|
||||
options["name"] = self.multiworld.player_name[self.player]
|
||||
option_writes = {
|
||||
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
|
||||
"enemies_density": cc(self.options.enemies_density),
|
||||
"chests_shuffle": "Include",
|
||||
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
|
||||
"shuffle_boxes_content": self.options.brown_boxes == "shuffle",
|
||||
"npcs_shuffle": "Include",
|
||||
"battlefields_shuffle": "Include",
|
||||
"logic_options": cc(self.multiworld.logic[self.player]),
|
||||
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
|
||||
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
|
||||
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
|
||||
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
|
||||
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
|
||||
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
|
||||
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
|
||||
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
|
||||
"logic_options": cc(self.options.logic),
|
||||
"shuffle_enemies_position": tf(self.options.shuffle_enemies_position),
|
||||
"enemies_scaling_lower": cc(self.options.enemies_scaling_lower),
|
||||
"enemies_scaling_upper": cc(self.options.enemies_scaling_upper),
|
||||
"bosses_scaling_lower": cc(self.options.bosses_scaling_lower),
|
||||
"bosses_scaling_upper": cc(self.options.bosses_scaling_upper),
|
||||
"enemizer_attacks": cc(self.options.enemizer_attacks),
|
||||
"leveling_curve": cc(self.options.leveling_curve),
|
||||
"battles_quantity": cc(self.options.battlefields_battles_quantities) if
|
||||
self.options.battlefields_battles_quantities.value < 5 else
|
||||
"RandomLow" if
|
||||
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
|
||||
self.options.battlefields_battles_quantities.value == 5 else
|
||||
"RandomHigh",
|
||||
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
|
||||
"shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards),
|
||||
"random_starting_weapon": True,
|
||||
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
|
||||
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
|
||||
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
|
||||
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
|
||||
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
|
||||
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
|
||||
"progressive_gear": tf(self.options.progressive_gear),
|
||||
"tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons),
|
||||
"doom_castle_mode": cc(self.options.doom_castle_mode),
|
||||
"doom_castle_shortcut": tf(self.options.doom_castle_shortcut),
|
||||
"sky_coin_mode": cc(self.options.sky_coin_mode),
|
||||
"sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity),
|
||||
"enable_spoilers": False,
|
||||
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
|
||||
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
|
||||
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
|
||||
"enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]),
|
||||
"shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]),
|
||||
"companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]),
|
||||
"companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]),
|
||||
"starting_companion": cc(self.multiworld.starting_companion[self.player]),
|
||||
"progressive_formations": cc(self.options.progressive_formations),
|
||||
"map_shuffling": cc(self.options.map_shuffle),
|
||||
"crest_shuffle": tf(self.options.crest_shuffle),
|
||||
"enemizer_groups": cc(self.options.enemizer_groups),
|
||||
"shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types),
|
||||
"companion_leveling_type": cc(self.options.companion_leveling_type),
|
||||
"companion_spellbook_type": cc(self.options.companion_spellbook_type),
|
||||
"starting_companion": cc(self.options.starting_companion),
|
||||
"available_companions": ["Zero", "One", "Two",
|
||||
"Three", "Four"][self.multiworld.available_companions[self.player].value],
|
||||
"companions_locations": cc(self.multiworld.companions_locations[self.player]),
|
||||
"kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]),
|
||||
"Three", "Four"][self.options.available_companions.value],
|
||||
"companions_locations": cc(self.options.companions_locations),
|
||||
"kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur),
|
||||
}
|
||||
|
||||
for option, data in option_writes.items():
|
||||
options["Final Fantasy Mystic Quest"][option][data] = 1
|
||||
|
||||
rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
|
||||
rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21]
|
||||
self.rom_name = bytearray(rom_name,
|
||||
'utf8')
|
||||
self.rom_name_available_event.set()
|
||||
|
||||
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
|
||||
hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
|
||||
hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
|
||||
|
||||
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
||||
starting_items.append("SkyCoin")
|
||||
|
||||
file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq")
|
||||
|
||||
+21
-26
@@ -1,11 +1,9 @@
|
||||
from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .data.rooms import rooms, entrances
|
||||
from .Items import item_groups, yaml_item
|
||||
import pkgutil
|
||||
import yaml
|
||||
|
||||
rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader)
|
||||
entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)}
|
||||
entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances}
|
||||
|
||||
object_id_table = {}
|
||||
object_type_table = {}
|
||||
@@ -69,7 +67,7 @@ def create_regions(self):
|
||||
location_table else None, object["type"], object["access"],
|
||||
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
|
||||
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
|
||||
"BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and
|
||||
"BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and
|
||||
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
|
||||
|
||||
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
|
||||
@@ -91,15 +89,13 @@ def create_regions(self):
|
||||
if "entrance" in link and link["entrance"] != -1:
|
||||
spoiler = False
|
||||
if link["entrance"] in crest_warps:
|
||||
if self.multiworld.crest_shuffle[self.player]:
|
||||
if self.options.crest_shuffle:
|
||||
spoiler = True
|
||||
elif self.multiworld.map_shuffle[self.player] == "everything":
|
||||
elif self.options.map_shuffle == "everything":
|
||||
spoiler = True
|
||||
elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons",
|
||||
"none"):
|
||||
elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"):
|
||||
spoiler = True
|
||||
elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none",
|
||||
"overworld"):
|
||||
elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"):
|
||||
spoiler = True
|
||||
|
||||
if spoiler:
|
||||
@@ -111,6 +107,7 @@ def create_regions(self):
|
||||
connection.connect(connect_room)
|
||||
break
|
||||
|
||||
|
||||
non_dead_end_crest_rooms = [
|
||||
'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room',
|
||||
'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge',
|
||||
@@ -140,7 +137,7 @@ def set_rules(self) -> None:
|
||||
add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic)
|
||||
add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic)
|
||||
|
||||
if self.multiworld.map_shuffle[self.player]:
|
||||
if self.options.map_shuffle:
|
||||
for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"):
|
||||
loc = self.multiworld.get_location(boss, self.player)
|
||||
checked_regions = {loc.parent_region}
|
||||
@@ -158,12 +155,12 @@ def set_rules(self) -> None:
|
||||
return True
|
||||
check_foresta(loc.parent_region)
|
||||
|
||||
if self.multiworld.logic[self.player] == "friendly":
|
||||
if self.options.logic == "friendly":
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||
["MagicMirror"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player),
|
||||
["Mask"])
|
||||
if self.multiworld.map_shuffle[self.player] in ("none", "overworld"):
|
||||
if self.options.map_shuffle in ("none", "overworld"):
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player),
|
||||
["Bomb"])
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player),
|
||||
@@ -185,8 +182,8 @@ def set_rules(self) -> None:
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player),
|
||||
["DragonClaw", "CaptainCap"])
|
||||
|
||||
if self.multiworld.logic[self.player] == "expert":
|
||||
if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]:
|
||||
if self.options.logic == "expert":
|
||||
if self.options.map_shuffle == "none" and not self.options.crest_shuffle:
|
||||
inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player)
|
||||
connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room)
|
||||
connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player))
|
||||
@@ -198,14 +195,14 @@ def set_rules(self) -> None:
|
||||
if entrance.connected_region.name in non_dead_end_crest_rooms:
|
||||
entrance.access_rule = lambda state: False
|
||||
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value]
|
||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
||||
logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value]
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has("Sky Fragment", self.player, logic_coins)
|
||||
elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals":
|
||||
elif self.options.sky_coin_mode == "save_the_crystals":
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player)
|
||||
elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"):
|
||||
elif self.options.sky_coin_mode in ("standard", "start_with"):
|
||||
self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \
|
||||
lambda state: state.has("Sky Coin", self.player)
|
||||
|
||||
@@ -213,26 +210,24 @@ def set_rules(self) -> None:
|
||||
def stage_set_rules(multiworld):
|
||||
# If there's no enemies, there's no repeatable income sources
|
||||
no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest")
|
||||
if multiworld.enemies_density[player] == "none"]
|
||||
if multiworld.worlds[player].options.enemies_density == "none"]
|
||||
if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler,
|
||||
ItemClassification.trap)]) > len([player for player in no_enemies_players if
|
||||
multiworld.accessibility[player] == "minimal"]) * 3):
|
||||
multiworld.worlds[player].options.accessibility == "minimal"]) * 3):
|
||||
for player in no_enemies_players:
|
||||
for location in vendor_locations:
|
||||
if multiworld.accessibility[player] == "locations":
|
||||
if multiworld.worlds[player].options.accessibility == "locations":
|
||||
multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
multiworld.get_location(location, player).access_rule = lambda state: False
|
||||
else:
|
||||
# There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing
|
||||
# advancement items so that useful items can be placed
|
||||
# advancement items so that useful items can be placed.
|
||||
for player in no_enemies_players:
|
||||
for location in vendor_locations:
|
||||
multiworld.get_location(location, player).item_rule = lambda item: not item.advancement
|
||||
|
||||
|
||||
|
||||
|
||||
class FFMQLocation(Location):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
|
||||
|
||||
+28
-34
@@ -10,7 +10,7 @@ from .Regions import create_regions, location_table, set_rules, stage_set_rules,
|
||||
non_dead_end_crest_warps
|
||||
from .Items import item_table, item_groups, create_items, FFMQItem, fillers
|
||||
from .Output import generate_output
|
||||
from .Options import option_definitions
|
||||
from .Options import FFMQOptions
|
||||
from .Client import FFMQClient
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ class FFMQWorld(World):
|
||||
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None}
|
||||
location_name_to_id = location_table
|
||||
option_definitions = option_definitions
|
||||
options_dataclass = FFMQOptions
|
||||
options: FFMQOptions
|
||||
|
||||
topology_present = True
|
||||
|
||||
@@ -67,20 +68,14 @@ class FFMQWorld(World):
|
||||
super().__init__(world, player)
|
||||
|
||||
def generate_early(self):
|
||||
if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin":
|
||||
self.multiworld.brown_boxes[self.player].value = 1
|
||||
if self.multiworld.enemies_scaling_lower[self.player].value > \
|
||||
self.multiworld.enemies_scaling_upper[self.player].value:
|
||||
(self.multiworld.enemies_scaling_lower[self.player].value,
|
||||
self.multiworld.enemies_scaling_upper[self.player].value) =\
|
||||
(self.multiworld.enemies_scaling_upper[self.player].value,
|
||||
self.multiworld.enemies_scaling_lower[self.player].value)
|
||||
if self.multiworld.bosses_scaling_lower[self.player].value > \
|
||||
self.multiworld.bosses_scaling_upper[self.player].value:
|
||||
(self.multiworld.bosses_scaling_lower[self.player].value,
|
||||
self.multiworld.bosses_scaling_upper[self.player].value) =\
|
||||
(self.multiworld.bosses_scaling_upper[self.player].value,
|
||||
self.multiworld.bosses_scaling_lower[self.player].value)
|
||||
if self.options.sky_coin_mode == "shattered_sky_coin":
|
||||
self.options.brown_boxes.value = 1
|
||||
if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value:
|
||||
self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \
|
||||
self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value
|
||||
if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value:
|
||||
self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \
|
||||
self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld):
|
||||
@@ -94,20 +89,20 @@ class FFMQWorld(World):
|
||||
rooms_data = {}
|
||||
|
||||
for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"):
|
||||
if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or
|
||||
world.multiworld.crest_shuffle[world.player]):
|
||||
if world.multiworld.map_shuffle_seed[world.player].value.isdigit():
|
||||
multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value))
|
||||
elif world.multiworld.map_shuffle_seed[world.player].value != "random":
|
||||
multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value))
|
||||
+ int(world.multiworld.seed))
|
||||
if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards
|
||||
or world.options.companions_locations):
|
||||
if world.options.map_shuffle_seed.value.isdigit():
|
||||
multiworld.random.seed(int(world.options.map_shuffle_seed.value))
|
||||
elif world.options.map_shuffle_seed.value != "random":
|
||||
multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value))
|
||||
+ int(world.multiworld.seed))
|
||||
|
||||
seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()
|
||||
map_shuffle = multiworld.map_shuffle[world.player].value
|
||||
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
|
||||
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
|
||||
companion_shuffle = multiworld.companions_locations[world.player].value
|
||||
kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key
|
||||
map_shuffle = world.options.map_shuffle.value
|
||||
crest_shuffle = world.options.crest_shuffle.current_key
|
||||
battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key
|
||||
companion_shuffle = world.options.companions_locations.value
|
||||
kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key
|
||||
|
||||
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
|
||||
|
||||
@@ -175,14 +170,14 @@ class FFMQWorld(World):
|
||||
|
||||
def extend_hint_information(self, hint_data):
|
||||
hint_data[self.player] = {}
|
||||
if self.multiworld.map_shuffle[self.player]:
|
||||
if self.options.map_shuffle:
|
||||
single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"]
|
||||
for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg",
|
||||
"Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship",
|
||||
"Subregion Doom Castle"]:
|
||||
region = self.multiworld.get_region(subregion, self.player)
|
||||
for location in region.locations:
|
||||
if location.address and self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||
if location.address and self.options.map_shuffle != "dungeons":
|
||||
hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1]
|
||||
+ (" Region" if subregion not in
|
||||
single_location_regions else ""))
|
||||
@@ -202,14 +197,13 @@ class FFMQWorld(World):
|
||||
for location in exit_check.connected_region.locations:
|
||||
if location.address:
|
||||
hint = []
|
||||
if self.multiworld.map_shuffle[self.player] != "dungeons":
|
||||
if self.options.map_shuffle != "dungeons":
|
||||
hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not
|
||||
in single_location_regions else "")))
|
||||
if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \
|
||||
("Subregion Mac's Ship", "Subregion Doom Castle"):
|
||||
if self.options.map_shuffle != "overworld":
|
||||
hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu",
|
||||
"Pazuzu's"))
|
||||
hint = " - ".join(hint)
|
||||
hint = " - ".join(hint).replace(" - Mac Ship", "")
|
||||
if location.address in hint_data[self.player]:
|
||||
hint_data[self.player][location.address] += f"/{hint}"
|
||||
else:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
+54
-13
@@ -116,12 +116,19 @@ class KH2Context(CommonContext):
|
||||
# self.inBattle = 0x2A0EAC4 + 0x40
|
||||
# self.onDeath = 0xAB9078
|
||||
# PC Address anchors
|
||||
self.Now = 0x0714DB8
|
||||
self.Save = 0x09A70B0
|
||||
# self.Now = 0x0714DB8 old address
|
||||
# epic addresses
|
||||
self.Now = 0x0716DF8
|
||||
self.Save = 0x09A92F0
|
||||
self.Journal = 0x743260
|
||||
self.Shop = 0x743350
|
||||
self.Slot1 = 0x2A22FD8
|
||||
# self.Sys3 = 0x2A59DF0
|
||||
# self.Bt10 = 0x2A74880
|
||||
# self.BtlEnd = 0x2A0D3E0
|
||||
self.Slot1 = 0x2A20C98
|
||||
# self.Slot1 = 0x2A20C98 old address
|
||||
|
||||
self.kh2_game_version = None # can be egs or steam
|
||||
|
||||
self.chest_set = set(exclusion_table["Chests"])
|
||||
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||
@@ -228,6 +235,9 @@ class KH2Context(CommonContext):
|
||||
def kh2_write_int(self, address, value):
|
||||
self.kh2.write_int(self.kh2.base_address + address, value)
|
||||
|
||||
def kh2_read_string(self, address, length):
|
||||
return self.kh2.read_string(self.kh2.base_address + address, length)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.kh2seedname = args['seed_name']
|
||||
@@ -367,10 +377,26 @@ class KH2Context(CommonContext):
|
||||
for weapon_location in all_weapon_slot:
|
||||
all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location])
|
||||
self.all_weapon_location_id = set(all_weapon_location_id)
|
||||
|
||||
try:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
logger.info("You are now auto-tracking")
|
||||
self.kh2connected = True
|
||||
if self.kh2_game_version is None:
|
||||
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
self.kh2_game_version = "STEAM"
|
||||
self.Now = 0x0717008
|
||||
self.Save = 0x09A9830
|
||||
self.Slot1 = 0x2A23518
|
||||
self.Journal = 0x7434E0
|
||||
self.Shop = 0x7435D0
|
||||
|
||||
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
self.kh2_game_version = "EGS"
|
||||
else:
|
||||
self.kh2_game_version = None
|
||||
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
|
||||
if self.kh2_game_version is not None:
|
||||
logger.info(f"You are now auto-tracking. {self.kh2_game_version}")
|
||||
self.kh2connected = True
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
@@ -589,8 +615,8 @@ class KH2Context(CommonContext):
|
||||
# if journal=-1 and shop = 5 then in shop
|
||||
# if journal !=-1 and shop = 10 then journal
|
||||
|
||||
journal = self.kh2_read_short(0x741230)
|
||||
shop = self.kh2_read_short(0x741320)
|
||||
journal = self.kh2_read_short(self.Journal)
|
||||
shop = self.kh2_read_short(self.Shop)
|
||||
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
# print("your in the shop")
|
||||
sellable_dict = {}
|
||||
@@ -599,8 +625,8 @@ class KH2Context(CommonContext):
|
||||
amount = self.kh2_read_byte(self.Save + itemdata.memaddr)
|
||||
sellable_dict[itemName] = amount
|
||||
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
journal = self.kh2_read_short(0x741230)
|
||||
shop = self.kh2_read_short(0x741320)
|
||||
journal = self.kh2_read_short(self.Journal)
|
||||
shop = self.kh2_read_short(self.Shop)
|
||||
await asyncio.sleep(0.5)
|
||||
for item, amount in sellable_dict.items():
|
||||
itemdata = self.item_name_to_data[item]
|
||||
@@ -750,7 +776,7 @@ class KH2Context(CommonContext):
|
||||
item_data = self.item_name_to_data[item_name]
|
||||
amount_of_items = 0
|
||||
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name]
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}:
|
||||
if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}:
|
||||
self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items)
|
||||
|
||||
for item_name in master_stat:
|
||||
@@ -802,7 +828,7 @@ class KH2Context(CommonContext):
|
||||
self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1)
|
||||
elif self.base_item_slots + amount_of_items < 8:
|
||||
self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items)
|
||||
|
||||
|
||||
# if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \
|
||||
# and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \
|
||||
# self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}:
|
||||
@@ -905,8 +931,23 @@ async def kh2_watcher(ctx: KH2Context):
|
||||
await asyncio.sleep(15)
|
||||
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
if ctx.kh2 is not None:
|
||||
logger.info("You are now auto-tracking")
|
||||
ctx.kh2connected = True
|
||||
if ctx.kh2_game_version is None:
|
||||
if ctx.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
ctx.kh2_game_version = "STEAM"
|
||||
ctx.Now = 0x0717008
|
||||
ctx.Save = 0x09A9830
|
||||
ctx.Slot1 = 0x2A23518
|
||||
ctx.Journal = 0x7434E0
|
||||
ctx.Shop = 0x7435D0
|
||||
|
||||
elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
ctx.kh2_game_version = "EGS"
|
||||
else:
|
||||
ctx.kh2_game_version = None
|
||||
logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.")
|
||||
if ctx.kh2_game_version is not None:
|
||||
logger.info(f"You are now auto-tracking {ctx.kh2_game_version}")
|
||||
ctx.kh2connected = True
|
||||
except Exception as e:
|
||||
if ctx.kh2connected:
|
||||
ctx.kh2connected = False
|
||||
|
||||
@@ -98,9 +98,12 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# Items can be grouped using their names to allow easy checking if any item
|
||||
# from that group has been collected. Group names can also be used for !hint
|
||||
#item_name_groups = {
|
||||
# "weapons": {"sword", "lance"}
|
||||
#}
|
||||
item_name_groups = {
|
||||
"Instruments": {
|
||||
"Full Moon Cello", "Conch Horn", "Sea Lily's Bell", "Surf Harp",
|
||||
"Wind Marimba", "Coral Triangle", "Organ of Evening Calm", "Thunder Drum"
|
||||
},
|
||||
}
|
||||
|
||||
prefill_dungeon_items = None
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Archipelago init file for Lingo
|
||||
"""
|
||||
from logging import warning
|
||||
|
||||
from BaseClasses import Item, ItemClassification, Tutorial
|
||||
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
|
||||
from Options import OptionError
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .datatypes import Room, RoomEntrance
|
||||
@@ -68,6 +68,37 @@ class LingoWorld(World):
|
||||
def create_regions(self):
|
||||
create_regions(self)
|
||||
|
||||
if not self.options.shuffle_postgame:
|
||||
state = CollectionState(self.multiworld)
|
||||
state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True)
|
||||
|
||||
# Note: relies on the assumption that real_items is a definitive list of real progression items in this
|
||||
# world, and is not modified after being created.
|
||||
for item in self.player_logic.real_items:
|
||||
state.collect(self.create_item(item), True)
|
||||
|
||||
# Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
|
||||
if self.player_logic.forced_good_item != "":
|
||||
state.collect(self.create_item(self.player_logic.forced_good_item), True)
|
||||
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
state.sweep_for_events(locations=all_locations)
|
||||
|
||||
unreachable_locations = [location for location in all_locations
|
||||
if not state.can_reach_location(location.name, self.player)]
|
||||
|
||||
for location in unreachable_locations:
|
||||
if location.name in self.player_logic.event_loc_to_item.keys():
|
||||
continue
|
||||
|
||||
self.player_logic.real_locations.remove(location.name)
|
||||
location.parent_region.locations.remove(location)
|
||||
|
||||
if len(self.player_logic.real_items) > len(self.player_logic.real_locations):
|
||||
raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number"
|
||||
f" of required items without shuffling the postgame. Either enable postgame"
|
||||
f" shuffling, or choose different options.")
|
||||
|
||||
def create_items(self):
|
||||
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
||||
|
||||
|
||||
@@ -140,6 +140,15 @@
|
||||
painting: True
|
||||
The Colorful:
|
||||
painting: True
|
||||
Welcome Back Area:
|
||||
room: Welcome Back Area
|
||||
door: Shortcut to Starting Room
|
||||
Second Room:
|
||||
door: Main Door
|
||||
Hidden Room:
|
||||
door: Back Right Door
|
||||
Rhyme Room (Looped Square):
|
||||
door: Rhyme Room Entrance
|
||||
panels:
|
||||
HI:
|
||||
id: Entry Room/Panel_hi_hi
|
||||
@@ -870,6 +879,8 @@
|
||||
panel: DRAWL + RUNS
|
||||
- room: Owl Hallway
|
||||
panel: READS + RUST
|
||||
- room: Ending Area
|
||||
panel: THE END
|
||||
paintings:
|
||||
- id: eye_painting
|
||||
disable: True
|
||||
@@ -2313,7 +2324,7 @@
|
||||
orientation: east
|
||||
- id: hi_solved_painting
|
||||
orientation: west
|
||||
Orange Tower Seventh Floor:
|
||||
Ending Area:
|
||||
entrances:
|
||||
Orange Tower Sixth Floor:
|
||||
room: Orange Tower
|
||||
@@ -2325,6 +2336,18 @@
|
||||
check: True
|
||||
tag: forbid
|
||||
non_counting: True
|
||||
location_name: Orange Tower Seventh Floor - THE END
|
||||
doors:
|
||||
End:
|
||||
event: True
|
||||
panels:
|
||||
- THE END
|
||||
Orange Tower Seventh Floor:
|
||||
entrances:
|
||||
Ending Area:
|
||||
room: Ending Area
|
||||
door: End
|
||||
panels:
|
||||
THE MASTER:
|
||||
# We will set up special rules for this in code.
|
||||
id: Countdown Panels/Panel_master_master
|
||||
|
||||
Binary file not shown.
@@ -272,8 +272,9 @@ panels:
|
||||
PAINTING (4): 445081
|
||||
PAINTING (5): 445082
|
||||
ROOM: 445083
|
||||
Orange Tower Seventh Floor:
|
||||
Ending Area:
|
||||
THE END: 444620
|
||||
Orange Tower Seventh Floor:
|
||||
THE MASTER: 444621
|
||||
MASTERY: 444622
|
||||
Behind A Smile:
|
||||
|
||||
@@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle):
|
||||
display_name = "Early Color Hallways"
|
||||
|
||||
|
||||
class ShufflePostgame(Toggle):
|
||||
"""When off, locations that could not be reached without also reaching your victory condition are removed."""
|
||||
display_name = "Shuffle Postgame"
|
||||
|
||||
|
||||
class TrapPercentage(Range):
|
||||
"""Replaces junk items with traps, at the specified rate."""
|
||||
display_name = "Trap Percentage"
|
||||
@@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions):
|
||||
mastery_achievements: MasteryAchievements
|
||||
level_2_requirement: Level2Requirement
|
||||
early_color_hallways: EarlyColorHallways
|
||||
shuffle_postgame: ShufflePostgame
|
||||
trap_percentage: TrapPercentage
|
||||
trap_weights: TrapWeights
|
||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||
|
||||
@@ -19,22 +19,25 @@ class AccessRequirements:
|
||||
doors: Set[RoomAndDoor]
|
||||
colors: Set[str]
|
||||
the_master: bool
|
||||
postgame: bool
|
||||
|
||||
def __init__(self):
|
||||
self.rooms = set()
|
||||
self.doors = set()
|
||||
self.colors = set()
|
||||
self.the_master = False
|
||||
self.postgame = False
|
||||
|
||||
def merge(self, other: "AccessRequirements"):
|
||||
self.rooms |= other.rooms
|
||||
self.doors |= other.doors
|
||||
self.colors |= other.colors
|
||||
self.the_master |= other.the_master
|
||||
self.postgame |= other.postgame
|
||||
|
||||
def __str__(self):
|
||||
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \
|
||||
f" the_master={self.the_master}"
|
||||
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
|
||||
f" the_master={self.the_master}, postgame={self.postgame})"
|
||||
|
||||
|
||||
class PlayerLocation(NamedTuple):
|
||||
@@ -190,16 +193,6 @@ class LingoPlayerLogic:
|
||||
if color_shuffle:
|
||||
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
|
||||
|
||||
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
|
||||
for room_name, room_data in PANELS_BY_ROOM.items():
|
||||
for panel_name, panel_data in room_data.items():
|
||||
if panel_data.achievement:
|
||||
access_req = AccessRequirements()
|
||||
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
||||
access_req.rooms.add(room_name)
|
||||
|
||||
self.mastery_reqs.append(access_req)
|
||||
|
||||
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
|
||||
# to prevent the actual victory condition from becoming a check.
|
||||
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
|
||||
@@ -207,7 +200,7 @@ class LingoPlayerLogic:
|
||||
|
||||
if victory_condition == VictoryCondition.option_the_end:
|
||||
self.victory_condition = "Orange Tower Seventh Floor - THE END"
|
||||
self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world)
|
||||
self.add_location("Ending Area", "The End (Solved)", None, [], world)
|
||||
self.event_loc_to_item["The End (Solved)"] = "Victory"
|
||||
elif victory_condition == VictoryCondition.option_the_master:
|
||||
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
|
||||
@@ -231,6 +224,16 @@ class LingoPlayerLogic:
|
||||
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
|
||||
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
|
||||
|
||||
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
|
||||
for room_name, room_data in PANELS_BY_ROOM.items():
|
||||
for panel_name, panel_data in room_data.items():
|
||||
if panel_data.achievement:
|
||||
access_req = AccessRequirements()
|
||||
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
||||
access_req.rooms.add(room_name)
|
||||
|
||||
self.mastery_reqs.append(access_req)
|
||||
|
||||
# Create groups of counting panel access requirements for the LEVEL 2 check.
|
||||
self.create_panel_hunt_events(world)
|
||||
|
||||
@@ -470,6 +473,11 @@ class LingoPlayerLogic:
|
||||
if panel == "THE MASTER":
|
||||
access_reqs.the_master = True
|
||||
|
||||
# Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name
|
||||
# override if it exists, or the auto-generated location name if it's None.
|
||||
if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"):
|
||||
access_reqs.postgame = True
|
||||
|
||||
self.panel_reqs[room][panel] = access_reqs
|
||||
|
||||
return self.panel_reqs[room][panel]
|
||||
|
||||
@@ -62,6 +62,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
|
||||
if access.the_master and not lingo_can_use_mastery_location(state, world):
|
||||
return False
|
||||
|
||||
if access.postgame and state.has("Prevent Victory", world.player):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
|
||||
options = {
|
||||
"mastery_achievements": "22",
|
||||
"victory_condition": "the_end",
|
||||
"shuffle_colors": "true"
|
||||
"shuffle_colors": "true",
|
||||
"shuffle_postgame": "true",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
@@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase):
|
||||
options = {
|
||||
"mastery_achievements": "24",
|
||||
"shuffle_colors": "true",
|
||||
"location_checks": "insanity"
|
||||
"location_checks": "insanity",
|
||||
"victory_condition": "level_2",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
|
||||
@@ -29,7 +29,6 @@ class TestPilgrimageWithRoofAndPaintings(LingoTestBase):
|
||||
"Outside The Undeterred - Green Painting"]
|
||||
|
||||
for door in doors:
|
||||
print(door)
|
||||
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
self.collect_by_name(door)
|
||||
|
||||
@@ -53,7 +52,6 @@ class TestPilgrimageNoRoofYesPaintings(LingoTestBase):
|
||||
"Starting Room - Street Painting"]
|
||||
|
||||
for door in doors:
|
||||
print(door)
|
||||
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
self.collect_by_name(door)
|
||||
|
||||
@@ -81,13 +79,40 @@ class TestPilgrimageNoRoofNoPaintings(LingoTestBase):
|
||||
"Orange Tower Fourth Floor - Hot Crusts Door"]
|
||||
|
||||
for door in doors:
|
||||
print(door)
|
||||
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
self.collect_by_name(door)
|
||||
|
||||
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
|
||||
|
||||
class TestPilgrimageRequireStartingRoom(LingoTestBase):
|
||||
options = {
|
||||
"enable_pilgrimage": "true",
|
||||
"shuffle_colors": "false",
|
||||
"shuffle_doors": "complex",
|
||||
"pilgrimage_allows_roof_access": "false",
|
||||
"pilgrimage_allows_paintings": "false",
|
||||
"early_color_hallways": "false"
|
||||
}
|
||||
|
||||
def test_access(self):
|
||||
doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance",
|
||||
"Outside The Undeterred - Green Painting", "Outside The Undeterred - Number Hunt",
|
||||
"Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room",
|
||||
"Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door",
|
||||
"Color Hunt - Shortcut to The Steady", "The Bearer - Entrance",
|
||||
"Orange Tower Fifth Floor - Quadruple Intersection", "The Tenacious - Shortcut to Hub Room",
|
||||
"Outside The Agreeable - Tenacious Entrance", "Crossroads - Tower Entrance",
|
||||
"Orange Tower Fourth Floor - Hot Crusts Door", "Challenge Room - Welcome Door",
|
||||
"Number Hunt - Challenge Entrance", "Welcome Back Area - Shortcut to Starting Room"]
|
||||
|
||||
for door in doors:
|
||||
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
self.collect_by_name(door)
|
||||
|
||||
self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
|
||||
|
||||
class TestPilgrimageYesRoofNoPaintings(LingoTestBase):
|
||||
options = {
|
||||
"enable_pilgrimage": "true",
|
||||
@@ -107,7 +132,6 @@ class TestPilgrimageYesRoofNoPaintings(LingoTestBase):
|
||||
"Orange Tower Fifth Floor - Quadruple Intersection"]
|
||||
|
||||
for door in doors:
|
||||
print(door)
|
||||
self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM"))
|
||||
self.collect_by_name(door)
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestPostgameVanillaTheEnd(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "none",
|
||||
"victory_condition": "the_end",
|
||||
"shuffle_postgame": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||
|
||||
self.assertTrue("The End (Solved)" in location_names)
|
||||
self.assertTrue("Champion's Rest - YOU" in location_names)
|
||||
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
|
||||
self.assertFalse("The Red - Achievement" in location_names)
|
||||
|
||||
|
||||
class TestPostgameComplexDoorsTheEnd(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"victory_condition": "the_end",
|
||||
"shuffle_postgame": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||
|
||||
self.assertTrue("The End (Solved)" in location_names)
|
||||
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
|
||||
self.assertTrue("The Red - Achievement" in location_names)
|
||||
|
||||
|
||||
class TestPostgameLateColorHunt(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "none",
|
||||
"victory_condition": "the_end",
|
||||
"sunwarp_access": "disabled",
|
||||
"shuffle_postgame": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||
|
||||
self.assertFalse("Champion's Rest - YOU" in location_names)
|
||||
|
||||
|
||||
class TestPostgameVanillaTheMaster(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "none",
|
||||
"victory_condition": "the_master",
|
||||
"shuffle_postgame": "false",
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||
|
||||
self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names)
|
||||
self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names)
|
||||
self.assertTrue("The Red - Achievement" in location_names)
|
||||
self.assertFalse("Mastery Panels" in location_names)
|
||||
@@ -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):
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -80,7 +80,7 @@ def generate_itempool(tlozworld):
|
||||
location.item.classification = ItemClassification.progression
|
||||
|
||||
def get_pool_core(world):
|
||||
random = world.multiworld.random
|
||||
random = world.random
|
||||
|
||||
pool = []
|
||||
placed_items = {}
|
||||
@@ -132,14 +132,6 @@ def get_pool_core(world):
|
||||
else:
|
||||
pool.append(fragment)
|
||||
|
||||
# Level 9 junk fill
|
||||
if world.options.ExpandedPool > 0:
|
||||
spots = random.sample(level_locations[8], len(level_locations[8]) // 2)
|
||||
for spot in spots:
|
||||
junk = random.choice(list(minor_items.keys()))
|
||||
placed_items[spot] = junk
|
||||
minor_items[junk] -= 1
|
||||
|
||||
# Finish Pool
|
||||
final_pool = basic_pool
|
||||
if world.options.ExpandedPool:
|
||||
|
||||
@@ -99,6 +99,14 @@ shop_locations = [
|
||||
"Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right"
|
||||
]
|
||||
|
||||
take_any_locations = [
|
||||
"Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"
|
||||
]
|
||||
|
||||
sword_cave_locations = [
|
||||
"Starting Sword Cave", "White Sword Pond", "Magical Sword Grave"
|
||||
]
|
||||
|
||||
food_locations = [
|
||||
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
|
||||
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
|
||||
|
||||
+18
-2
@@ -12,7 +12,8 @@ from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassi
|
||||
from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations
|
||||
from .Items import item_table, item_prices, item_game_ids
|
||||
from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \
|
||||
standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations
|
||||
standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations, \
|
||||
take_any_locations, sword_cave_locations
|
||||
from .Options import TlozOptions
|
||||
from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late
|
||||
from .Rules import set_rules
|
||||
@@ -87,6 +88,21 @@ class TLoZWorld(World):
|
||||
}
|
||||
}
|
||||
|
||||
location_name_groups = {
|
||||
"Shops": set(shop_locations),
|
||||
"Take Any": set(take_any_locations),
|
||||
"Sword Caves": set(sword_cave_locations),
|
||||
"Level 1": set(level_locations[0]),
|
||||
"Level 2": set(level_locations[1]),
|
||||
"Level 3": set(level_locations[2]),
|
||||
"Level 4": set(level_locations[3]),
|
||||
"Level 5": set(level_locations[4]),
|
||||
"Level 6": set(level_locations[5]),
|
||||
"Level 7": set(level_locations[6]),
|
||||
"Level 8": set(level_locations[7]),
|
||||
"Level 9": set(level_locations[8])
|
||||
}
|
||||
|
||||
for k, v in item_name_to_id.items():
|
||||
item_name_to_id[k] = v + base_id
|
||||
|
||||
@@ -307,7 +323,7 @@ class TLoZWorld(World):
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.filler_items is None:
|
||||
self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler]
|
||||
return self.multiworld.random.choice(self.filler_items)
|
||||
return self.random.choice(self.filler_items)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
if self.options.ExpandedPool:
|
||||
|
||||
+23
-18
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, List, Any, Tuple, TypedDict
|
||||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
|
||||
from logging import warning
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
|
||||
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
|
||||
@@ -12,6 +12,14 @@ from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_opt
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Options import PlandoConnection
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from settings import Group, Bool
|
||||
|
||||
|
||||
class TunicSettings(Group):
|
||||
class DisableLocalSpoiler(Bool):
|
||||
"""Disallows the TUNIC client from creating a local spoiler log."""
|
||||
|
||||
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
|
||||
|
||||
|
||||
class TunicWeb(WebWorld):
|
||||
@@ -57,6 +65,7 @@ class TunicWorld(World):
|
||||
|
||||
options: TunicOptions
|
||||
options_dataclass = TunicOptions
|
||||
settings: ClassVar[TunicSettings]
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = location_name_groups
|
||||
|
||||
@@ -151,9 +160,9 @@ class TunicWorld(World):
|
||||
if new_cxn:
|
||||
cls.seed_groups[group]["plando"].value.append(cxn)
|
||||
|
||||
def create_item(self, name: str) -> TunicItem:
|
||||
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
|
||||
item_data = item_table[name]
|
||||
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player)
|
||||
return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
|
||||
@@ -183,14 +192,12 @@ class TunicWorld(World):
|
||||
self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels)
|
||||
elif self.options.laurels_location == "10_fairies":
|
||||
self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels)
|
||||
self.slot_data_items.append(laurels)
|
||||
items_to_create["Hero's Laurels"] = 0
|
||||
|
||||
if self.options.keys_behind_bosses:
|
||||
for rgb_hexagon, location in hexagon_locations.items():
|
||||
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
|
||||
self.multiworld.get_location(location, self.player).place_locked_item(hex_item)
|
||||
self.slot_data_items.append(hex_item)
|
||||
items_to_create[rgb_hexagon] = 0
|
||||
items_to_create[gold_hexagon] -= 3
|
||||
|
||||
@@ -236,33 +243,30 @@ class TunicWorld(World):
|
||||
remove_filler(items_to_create[gold_hexagon])
|
||||
|
||||
for hero_relic in item_name_groups["Hero Relics"]:
|
||||
relic_item = TunicItem(hero_relic, ItemClassification.useful, self.item_name_to_id[hero_relic], self.player)
|
||||
tunic_items.append(relic_item)
|
||||
tunic_items.append(self.create_item(hero_relic, ItemClassification.useful))
|
||||
items_to_create[hero_relic] = 0
|
||||
|
||||
if not self.options.ability_shuffling:
|
||||
for page in item_name_groups["Abilities"]:
|
||||
if items_to_create[page] > 0:
|
||||
page_item = TunicItem(page, ItemClassification.useful, self.item_name_to_id[page], self.player)
|
||||
tunic_items.append(page_item)
|
||||
tunic_items.append(self.create_item(page, ItemClassification.useful))
|
||||
items_to_create[page] = 0
|
||||
|
||||
if self.options.maskless:
|
||||
mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player)
|
||||
tunic_items.append(mask_item)
|
||||
tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful))
|
||||
items_to_create["Scavenger Mask"] = 0
|
||||
|
||||
if self.options.lanternless:
|
||||
lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player)
|
||||
tunic_items.append(lantern_item)
|
||||
tunic_items.append(self.create_item("Lantern", ItemClassification.useful))
|
||||
items_to_create["Lantern"] = 0
|
||||
|
||||
for item, quantity in items_to_create.items():
|
||||
for _ in range(quantity):
|
||||
tunic_item: TunicItem = self.create_item(item)
|
||||
if item in slot_data_item_names:
|
||||
self.slot_data_items.append(tunic_item)
|
||||
tunic_items.append(tunic_item)
|
||||
tunic_items.append(self.create_item(item))
|
||||
|
||||
for tunic_item in tunic_items:
|
||||
if tunic_item.name in slot_data_item_names:
|
||||
self.slot_data_items.append(tunic_item)
|
||||
|
||||
self.multiworld.itempool += tunic_items
|
||||
|
||||
@@ -373,7 +377,8 @@ class TunicWorld(World):
|
||||
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
|
||||
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],
|
||||
"Hexagon Quest Goal": self.options.hexagon_goal.value,
|
||||
"Entrance Rando": self.tunic_portal_pairs
|
||||
"Entrance Rando": self.tunic_portal_pairs,
|
||||
"disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race),
|
||||
}
|
||||
|
||||
for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):
|
||||
|
||||
@@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
|
||||
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
|
||||
|
||||
Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
|
||||
158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
158328 - 0x09DB8 (Boat Spawn) - True - Boat
|
||||
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
|
||||
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
|
||||
@@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB
|
||||
158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
|
||||
|
||||
Elevator (Mountain Bottom Floor):
|
||||
158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
|
||||
158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
|
||||
158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
|
||||
158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
|
||||
158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
|
||||
|
||||
@@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
|
||||
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
|
||||
|
||||
Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
|
||||
158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
158328 - 0x09DB8 (Boat Spawn) - True - Boat
|
||||
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots
|
||||
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots
|
||||
@@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB
|
||||
158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots
|
||||
|
||||
Elevator (Mountain Bottom Floor):
|
||||
158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
|
||||
158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
|
||||
158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
|
||||
158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
|
||||
158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
|
||||
|
||||
@@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near
|
||||
159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True
|
||||
|
||||
Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8:
|
||||
158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True
|
||||
158328 - 0x09DB8 (Boat Spawn) - True - Boat
|
||||
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
|
||||
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
|
||||
@@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB
|
||||
158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry
|
||||
|
||||
Elevator (Mountain Bottom Floor):
|
||||
158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True
|
||||
158530 - 0x3D9A6 (Elevator Door Close Left) - True - True
|
||||
158531 - 0x3D9A7 (Elevator Door Close Right) - True - True
|
||||
158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True
|
||||
158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True
|
||||
|
||||
Reference in New Issue
Block a user