Merge branch 'main' into tunc-portal-direction-pairing

This commit is contained in:
Scipio Wright
2024-09-13 10:18:31 -04:00
committed by GitHub
28 changed files with 436 additions and 269 deletions

View File

@@ -662,7 +662,7 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> type:
def make_gui(self) -> typing.Type["kvui.GameManager"]:
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
from kvui import GameManager
@@ -1035,15 +1035,18 @@ def run_as_textclient(*args):
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable
args = parser.parse_args(args)
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
if url.scheme == "archipelago":
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
colorama.init()
@@ -1053,4 +1056,4 @@ def run_as_textclient(*args):
if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
run_as_textclient()
run_as_textclient(*sys.argv[1:]) # default value for parse_args

View File

@@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.outputpath = args.outputpath
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
erargs.name = {}
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
@@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename
elif player not in erargs.name: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)

View File

@@ -401,7 +401,10 @@ if __name__ == '__main__':
init_logging('Launcher')
Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser(description='Archipelago Launcher')
parser = argparse.ArgumentParser(
description='Archipelago Launcher',
usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]"
)
run_group = parser.add_argument_group("Run")
run_group.add_argument("--update_settings", action="store_true",
help="Update host.yaml and exit.")

View File

@@ -973,7 +973,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
if random.random() < float(text.get("percentage", 100)/100):
at = text.get("at", None)
if at is not None:
if isinstance(at, dict):
if at:
at = random.choices(list(at.keys()),
weights=list(at.values()), k=1)[0]
else:
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
given_text = text.get("text", [])
if isinstance(given_text, dict):
if not given_text:
given_text = []
else:
given_text = random.choices(list(given_text.keys()),
weights=list(given_text.values()), k=1)
if isinstance(given_text, str):
given_text = [given_text]
texts.append(PlandoText(
@@ -981,6 +993,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
given_text,
text.get("percentage", 100)
))
else:
raise OptionError("\"at\" must be a valid string or weighted list of strings!")
elif isinstance(text, PlandoText):
if random.random() < float(text.percentage/100):
texts.append(text)

View File

@@ -46,7 +46,7 @@
/worlds/clique/ @ThePhar
# Dark Souls III
/worlds/dark_souls_3/ @Marechal-L
/worlds/dark_souls_3/ @Marechal-L @nex3
# Donkey Kong Country 3
/worlds/dkc3/ @PoryGone

View File

@@ -61,7 +61,7 @@ class Component:
processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()):
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
@@ -85,7 +85,7 @@ class SuffixIdentifier:
def launch_textclient(*args):
import CommonClient
launch_subprocess(CommonClient.run_as_textclient, "TextClient", args)
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:

View File

@@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext):
self.bizhawk_ctx = BizHawkContext()
self.watcher_timeout = 0.5
def run_gui(self):
from kvui import GameManager
class BizHawkManager(GameManager):
base_title = "Archipelago BizHawk Client"
self.ui = BizHawkManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def make_gui(self):
ui = super().make_gui()
ui.base_title = "Archipelago BizHawk Client"
return ui
def on_package(self, cmd, args):
if cmd == "Connected":

View File

@@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections):
entrances = set([connection[0] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)])
exits = set([connection[1] for connection in (
exits = set([connection[0] for connection in (
*default_connections, *default_dungeon_connections, *inverted_default_connections,
*inverted_default_dungeon_connections)])

View File

@@ -27,6 +27,7 @@ class MessengerSettings(Group):
class GamePath(FilePath):
description = "The Messenger game executable"
is_exe = True
md5s = ["1b53534569060bc06179356cd968ed1d"]
game_path: GamePath = GamePath("TheMessenger.exe")

View File

@@ -5,7 +5,6 @@ import os.path
import subprocess
import urllib.request
from shutil import which
from tkinter.messagebox import askyesnocancel
from typing import Any, Optional
from zipfile import ZipFile
from Utils import open_file
@@ -18,11 +17,33 @@ from Utils import is_windows, messagebox, tuplize_version
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
"""
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
:param title: Title to be displayed at the top of the message box.
:param text: Text to be displayed inside the message box.
:return: Returns True if yes, False if no, None if cancel.
"""
from tkinter import Tk, messagebox
root = Tk()
root.withdraw()
ret = messagebox.askyesnocancel(title, text)
root.update()
return ret
def launch_game(*args) -> None:
"""Check the game installation, then launch it"""
def courier_installed() -> bool:
"""Check if Courier is installed"""
return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll"))
assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
with open(assembly_path, "rb") as assembly:
for line in assembly:
if b"Courier" in line:
return True
return False
def mod_installed() -> bool:
"""Check if the mod is installed"""
@@ -57,27 +78,34 @@ def launch_game(*args) -> None:
if not is_windows:
mono_exe = which("mono")
if not mono_exe:
# steam deck support but doesn't currently work
messagebox("Failure", "Failed to install Courier", True)
raise RuntimeError("Failed to install Courier")
# # download and use mono kickstart
# # this allows steam deck support
# mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip"
# target = os.path.join(folder, "monoKickstart")
# os.makedirs(target, exist_ok=True)
# with urllib.request.urlopen(mono_kick_url) as download:
# with ZipFile(io.BytesIO(download.read()), "r") as zf:
# for member in zf.infolist():
# zf.extract(member, path=target)
# installer = subprocess.Popen([os.path.join(target, "precompiled"),
# os.path.join(folder, "MiniInstaller.exe")], shell=False)
# os.remove(target)
# download and use mono kickstart
# this allows steam deck support
mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip"
files = []
with urllib.request.urlopen(mono_kick_url) as download:
with ZipFile(io.BytesIO(download.read()), "r") as zf:
for member in zf.infolist():
if "precompiled/" not in member.filename or member.filename.endswith("/"):
continue
member.filename = member.filename.split("/")[-1]
if member.filename.endswith("bin.x86_64"):
member.filename = "MiniInstaller.bin.x86_64"
zf.extract(member, path=game_folder)
files.append(member.filename)
mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64")
os.chmod(mono_installer, 0o755)
installer = subprocess.Popen(mono_installer, shell=False)
failure = installer.wait()
for file in files:
os.remove(file)
else:
installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False)
installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True)
failure = installer.wait()
else:
installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False)
installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True)
failure = installer.wait()
failure = installer.wait()
print(failure)
if failure:
messagebox("Failure", "Failed to install Courier", True)
os.chdir(working_directory)
@@ -125,18 +153,35 @@ def launch_game(*args) -> None:
return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version)
from . import MessengerWorld
game_folder = os.path.dirname(MessengerWorld.settings.game_path)
try:
game_folder = os.path.dirname(MessengerWorld.settings.game_path)
except ValueError as e:
logging.error(e)
messagebox("Invalid File", "Selected file did not match expected hash. "
"Please try again and ensure you select The Messenger.exe.")
return
working_directory = os.getcwd()
# setup ssl context
try:
import certifi
import ssl
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
context.set_alpn_protocols(["http/1.1"])
https_handler = urllib.request.HTTPSHandler(context=context)
opener = urllib.request.build_opener(https_handler)
urllib.request.install_opener(opener)
except ImportError:
pass
if not courier_installed():
should_install = askyesnocancel("Install Courier",
"No Courier installation detected. Would you like to install now?")
should_install = ask_yes_no_cancel("Install Courier",
"No Courier installation detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Courier")
install_courier()
if not mod_installed():
should_install = askyesnocancel("Install Mod",
"No randomizer mod detected. Would you like to install now?")
should_install = ask_yes_no_cancel("Install Mod",
"No randomizer mod detected. Would you like to install now?")
if not should_install:
return
logging.info("Installing Mod")
@@ -144,17 +189,24 @@ def launch_game(*args) -> None:
else:
latest = request_data(MOD_URL)["tag_name"]
if available_mod_update(latest):
should_update = askyesnocancel("Update Mod",
f"New mod version detected. Would you like to update to {latest} now?")
should_update = ask_yes_no_cancel("Update Mod",
f"New mod version detected. Would you like to update to {latest} now?")
if should_update:
logging.info("Updating mod")
install_mod()
elif should_update is None:
return
if not args:
should_launch = ask_yes_no_cancel("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?")
if not should_launch:
return
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
args = parser.parse_args(args)
if not is_windows:
if args.url:
open_file(f"steam://rungameid/764790//{args.url}/")

View File

@@ -711,6 +711,7 @@ class PokemonEmeraldWorld(World):
"trainersanity",
"modify_118",
"death_link",
"normalize_encounter_rates",
)
slot_data["free_fly_location_id"] = self.free_fly_location_id
slot_data["hm_requirements"] = self.hm_requirements

View File

@@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum:
return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES
@dataclass
class TrainerPokemonData:
class TrainerPokemonData(NamedTuple):
species_id: int
level: int
moves: Optional[Tuple[int, int, int, int]]
@dataclass
class TrainerPartyData:
class TrainerPartyData(NamedTuple):
pokemon: List[TrainerPokemonData]
pokemon_data_type: TrainerPokemonDataTypeEnum
address: int

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, Dict, List, Set
from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data
from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data
from .options import RandomizeTrainerParties
from .pokemon import filter_species_by_nearby_bst
from .util import int_to_bool_array
@@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3]
)
new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves))
trainer.party.pokemon = new_party
trainer.party = trainer.party._replace(pokemon=new_party)

View File

@@ -4,8 +4,7 @@ Functions related to pokemon species and moves
import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData,
SpeciesData, data)
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data)
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
@@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
type_bias, normal_bias, species.types)
else:
new_move = 0
new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
cursor += 1
# All moves from here onward are actual moves.
@@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
new_move = get_random_move(world.random,
{move.move_id for move in new_learnset} | world.blacklisted_moves,
type_bias, normal_bias, species.types)
new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
cursor += 1
species.learnset = new_learnset
@@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None:
picked_evolution = world.random.choice(potential_evolutions)
for trainer_name, starter_position, is_evolved in rival_teams[i]:
new_species_id = picked_evolution if is_evolved else starter.species_id
trainer_data = world.modified_trainers[data.constants[trainer_name]]
trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
trainer_data.party.pokemon[starter_position] = \
trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id)
def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
@@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
world.random.shuffle(shuffled_species)
for i, encounter in enumerate(data.legendary_encounters):
world.modified_legendary_encounters.append(MiscPokemonData(
shuffled_species[i],
encounter.address
))
world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i]))
else:
should_match_bst = world.options.legendary_encounters in {
RandomizeLegendaryEncounters.option_match_base_stats,
@@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
world.modified_legendary_encounters.append(MiscPokemonData(
world.random.choice(candidates).species_id,
encounter.address
world.modified_legendary_encounters.append(encounter._replace(
species_id=world.random.choice(candidates).species_id
))
@@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
world.modified_misc_pokemon = []
for i, encounter in enumerate(data.misc_pokemon):
world.modified_misc_pokemon.append(MiscPokemonData(
shuffled_species[i],
encounter.address
))
world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i]))
else:
should_match_bst = world.options.misc_pokemon in {
RandomizeMiscPokemon.option_match_base_stats,
@@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
if len(player_filtered_candidates) > 0:
candidates = player_filtered_candidates
world.modified_misc_pokemon.append(MiscPokemonData(
world.random.choice(candidates).species_id,
encounter.address
world.modified_misc_pokemon.append(encounter._replace(
species_id=world.random.choice(candidates).species_id
))

View File

@@ -1,6 +1,6 @@
from typing import Dict
from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet
from dataclasses import dataclass
class StartingGender(Choice):
@@ -175,13 +175,21 @@ class NumberOfChildren(Range):
default = 3
class AdditionalNames(OptionSet):
class AdditionalLadyNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
Gender.
"""
display_name = "Additional Names"
display_name = "Additional Lady Names"
class AdditionalSirNames(OptionSet):
"""
Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list
of names your children can have. The first value will also be your initial character's name depending on Starting
Gender.
"""
display_name = "Additional Sir Names"
class AllowDefaultNames(DefaultOnToggle):
@@ -336,42 +344,44 @@ class AvailableClasses(OptionSet):
The upgraded form of your starting class will be available regardless.
"""
display_name = "Available Classes"
default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
default = frozenset(
{"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
)
valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"}
rl_options: Dict[str, type(Option)] = {
"starting_gender": StartingGender,
"starting_class": StartingClass,
"available_classes": AvailableClasses,
"new_game_plus": NewGamePlus,
"fairy_chests_per_zone": FairyChestsPerZone,
"chests_per_zone": ChestsPerZone,
"universal_fairy_chests": UniversalFairyChests,
"universal_chests": UniversalChests,
"vendors": Vendors,
"architect": Architect,
"architect_fee": ArchitectFee,
"disable_charon": DisableCharon,
"require_purchasing": RequirePurchasing,
"progressive_blueprints": ProgressiveBlueprints,
"gold_gain_multiplier": GoldGainMultiplier,
"number_of_children": NumberOfChildren,
"free_diary_on_generation": FreeDiaryOnGeneration,
"khidr": ChallengeBossKhidr,
"alexander": ChallengeBossAlexander,
"leon": ChallengeBossLeon,
"herodotus": ChallengeBossHerodotus,
"health_pool": HealthUpPool,
"mana_pool": ManaUpPool,
"attack_pool": AttackUpPool,
"magic_damage_pool": MagicDamageUpPool,
"armor_pool": ArmorUpPool,
"equip_pool": EquipUpPool,
"crit_chance_pool": CritChanceUpPool,
"crit_damage_pool": CritDamageUpPool,
"allow_default_names": AllowDefaultNames,
"additional_lady_names": AdditionalNames,
"additional_sir_names": AdditionalNames,
"death_link": DeathLink,
}
@dataclass
class RLOptions(PerGameCommonOptions):
starting_gender: StartingGender
starting_class: StartingClass
available_classes: AvailableClasses
new_game_plus: NewGamePlus
fairy_chests_per_zone: FairyChestsPerZone
chests_per_zone: ChestsPerZone
universal_fairy_chests: UniversalFairyChests
universal_chests: UniversalChests
vendors: Vendors
architect: Architect
architect_fee: ArchitectFee
disable_charon: DisableCharon
require_purchasing: RequirePurchasing
progressive_blueprints: ProgressiveBlueprints
gold_gain_multiplier: GoldGainMultiplier
number_of_children: NumberOfChildren
free_diary_on_generation: FreeDiaryOnGeneration
khidr: ChallengeBossKhidr
alexander: ChallengeBossAlexander
leon: ChallengeBossLeon
herodotus: ChallengeBossHerodotus
health_pool: HealthUpPool
mana_pool: ManaUpPool
attack_pool: AttackUpPool
magic_damage_pool: MagicDamageUpPool
armor_pool: ArmorUpPool
equip_pool: EquipUpPool
crit_chance_pool: CritChanceUpPool
crit_damage_pool: CritDamageUpPool
allow_default_names: AllowDefaultNames
additional_lady_names: AdditionalLadyNames
additional_sir_names: AdditionalSirNames
death_link: DeathLink

View File

@@ -1,15 +1,18 @@
from typing import Dict, List, NamedTuple, Optional
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import MultiWorld, Region, Entrance
from .Locations import RLLocation, location_table, get_locations_by_category
if TYPE_CHECKING:
from . import RLWorld
class RLRegionData(NamedTuple):
locations: Optional[List[str]]
region_exits: Optional[List[str]]
def create_regions(multiworld: MultiWorld, player: int):
def create_regions(world: "RLWorld"):
regions: Dict[str, RLRegionData] = {
"Menu": RLRegionData(None, ["Castle Hamson"]),
"The Manor": RLRegionData([], []),
@@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int):
regions["The Fountain Room"].locations.append("Fountain Room")
# Chests
chests = int(multiworld.chests_per_zone[player])
chests = int(world.options.chests_per_zone)
for i in range(0, chests):
if multiworld.universal_chests[player]:
if world.options.universal_chests:
regions["Castle Hamson"].locations.append(f"Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}")
@@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int):
regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}")
# Fairy Chests
chests = int(multiworld.fairy_chests_per_zone[player])
chests = int(world.options.fairy_chests_per_zone)
for i in range(0, chests):
if multiworld.universal_fairy_chests[player]:
if world.options.universal_fairy_chests:
regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}")
regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}")
regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}")
@@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int):
# Set up the regions correctly.
for name, data in regions.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data))
multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player))
multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player))
multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player))
multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player))
multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player))
multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player))
world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson"))
world.get_entrance("The Manor").connect(world.get_region("The Manor"))
world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia"))
world.get_entrance("The Maya").connect(world.get_region("The Maya"))
world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness"))
world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room"))
def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData):

View File

@@ -1,9 +1,13 @@
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import RLWorld
def get_upgrade_total(multiworld: MultiWorld, player: int) -> int:
return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \
int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player])
def get_upgrade_total(world: "RLWorld") -> int:
return int(world.options.health_pool) + int(world.options.mana_pool) + \
int(world.options.attack_pool) + int(world.options.magic_damage_pool)
def get_upgrade_count(state: CollectionState, player: int) -> int:
@@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool
return get_upgrade_count(state, player) >= amount
def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool:
return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100)))
def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool:
return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100)))
def has_movement_rune(state: CollectionState, player: int) -> bool:
@@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool:
return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player)
def set_rules(multiworld: MultiWorld, player: int):
def set_rules(world: "RLWorld", player: int):
# If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres.
if multiworld.vendors[player] == "normal":
multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \
if world.options.vendors == "normal":
world.get_location("Forest Abkhazia Boss Reward").access_rule = \
lambda state: has_vendors(state, player)
# Gate each manor location so everything isn't dumped into sphere 1.
manor_rules = {
"Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [
"Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [
"Manor - Left Wing Window",
"Manor - Left Wing Rooftop",
"Manor - Right Wing Window",
@@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int):
"Manor - Left Tree 2",
"Manor - Right Tree",
],
"Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [
"Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [
"Manor - Left Big Upper 1",
"Manor - Left Big Upper 2",
"Manor - Left Big Windows",
@@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int):
"Manor - Right Big Rooftop",
"Manor - Right Extension",
],
"Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [
"Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [
"Manor - Right High Base",
"Manor - Right High Upper",
"Manor - Right High Tower",
@@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int):
# Set rules for manor locations.
for event, locations in manor_rules.items():
for location in locations:
multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player)
world.get_location(location).access_rule = lambda state: state.has(event, player)
# Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests.
for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]:
for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]:
fairy_location.access_rule = lambda state: has_fairy_progression(state, player)
# Region rules.
multiworld.get_entrance("Forest Abkhazia", player).access_rule = \
lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player)
world.get_entrance("Forest Abkhazia").access_rule = \
lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player)
multiworld.get_entrance("The Maya", player).access_rule = \
lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player)
world.get_entrance("The Maya").access_rule = \
lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player)
multiworld.get_entrance("Land of Darkness", player).access_rule = \
lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player)
world.get_entrance("Land of Darkness").access_rule = \
lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player)
multiworld.get_entrance("The Fountain Room", player).access_rule = \
lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player)
world.get_entrance("The Fountain Room").access_rule = \
lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player)
# Win condition.
multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)
world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player)

View File

@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
from .Locations import RLLocation, location_table
from .Options import rl_options
from .Options import RLOptions
from .Presets import rl_options_presets
from .Regions import create_regions
from .Rules import set_rules
@@ -33,20 +33,17 @@ class RLWorld(World):
But that's OK, because no one is perfect, and you don't have to be to succeed.
"""
game = "Rogue Legacy"
option_definitions = rl_options
options_dataclass = RLOptions
options: RLOptions
topology_present = True
required_client_version = (0, 3, 5)
web = RLWeb()
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.code for name, data in location_table.items()}
# TODO: Replace calls to this function with "options-dict", once that PR is completed and merged.
def get_setting(self, name: str):
return getattr(self.multiworld, name)[self.player]
item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None}
location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None}
def fill_slot_data(self) -> dict:
return {option_name: self.get_setting(option_name).value for option_name in rl_options}
return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()])
def generate_early(self):
location_ids_used_per_game = {
@@ -74,18 +71,18 @@ class RLWorld(World):
)
# Check validation of names.
additional_lady_names = len(self.get_setting("additional_lady_names").value)
additional_sir_names = len(self.get_setting("additional_sir_names").value)
if not self.get_setting("allow_default_names"):
if additional_lady_names < int(self.get_setting("number_of_children")):
additional_lady_names = len(self.options.additional_lady_names.value)
additional_sir_names = len(self.options.additional_sir_names.value)
if not self.options.allow_default_names:
if additional_lady_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_lady_names. "
f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}")
f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}")
if additional_sir_names < int(self.get_setting("number_of_children")):
if additional_sir_names < int(self.options.number_of_children):
raise Exception(
f"allow_default_names is off, but not enough names are defined in additional_sir_names. "
f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}")
f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}")
def create_items(self):
item_pool: List[RLItem] = []
@@ -95,110 +92,110 @@ class RLWorld(World):
# Architect
if name == "Architect":
if self.get_setting("architect") == "disabled":
if self.options.architect == "disabled":
continue
if self.get_setting("architect") == "start_unlocked":
if self.options.architect == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
if self.get_setting("architect") == "early":
if self.options.architect == "early":
self.multiworld.local_early_items[self.player]["Architect"] = 1
# Blacksmith and Enchantress
if name == "Blacksmith" or name == "Enchantress":
if self.get_setting("vendors") == "start_unlocked":
if self.options.vendors == "start_unlocked":
self.multiworld.push_precollected(self.create_item(name))
continue
if self.get_setting("vendors") == "early":
if self.options.vendors == "early":
self.multiworld.local_early_items[self.player]["Blacksmith"] = 1
self.multiworld.local_early_items[self.player]["Enchantress"] = 1
# Haggling
if name == "Haggling" and self.get_setting("disable_charon"):
if name == "Haggling" and self.options.disable_charon:
continue
# Blueprints
if data.category == "Blueprints":
# No progressive blueprints if progressive_blueprints are disabled.
if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"):
if name == "Progressive Blueprints" and not self.options.progressive_blueprints:
continue
# No distinct blueprints if progressive_blueprints are enabled.
elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"):
elif name != "Progressive Blueprints" and self.options.progressive_blueprints:
continue
# Classes
if data.category == "Classes":
if name == "Progressive Knights":
if "Knight" not in self.get_setting("available_classes"):
if "Knight" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "knight":
if self.options.starting_class == "knight":
quantity = 1
if name == "Progressive Mages":
if "Mage" not in self.get_setting("available_classes"):
if "Mage" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "mage":
if self.options.starting_class == "mage":
quantity = 1
if name == "Progressive Barbarians":
if "Barbarian" not in self.get_setting("available_classes"):
if "Barbarian" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "barbarian":
if self.options.starting_class == "barbarian":
quantity = 1
if name == "Progressive Knaves":
if "Knave" not in self.get_setting("available_classes"):
if "Knave" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "knave":
if self.options.starting_class == "knave":
quantity = 1
if name == "Progressive Miners":
if "Miner" not in self.get_setting("available_classes"):
if "Miner" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "miner":
if self.options.starting_class == "miner":
quantity = 1
if name == "Progressive Shinobis":
if "Shinobi" not in self.get_setting("available_classes"):
if "Shinobi" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "shinobi":
if self.options.starting_class == "shinobi":
quantity = 1
if name == "Progressive Liches":
if "Lich" not in self.get_setting("available_classes"):
if "Lich" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "lich":
if self.options.starting_class == "lich":
quantity = 1
if name == "Progressive Spellthieves":
if "Spellthief" not in self.get_setting("available_classes"):
if "Spellthief" not in self.options.available_classes:
continue
if self.get_setting("starting_class") == "spellthief":
if self.options.starting_class == "spellthief":
quantity = 1
if name == "Dragons":
if "Dragon" not in self.get_setting("available_classes"):
if "Dragon" not in self.options.available_classes:
continue
if name == "Traitors":
if "Traitor" not in self.get_setting("available_classes"):
if "Traitor" not in self.options.available_classes:
continue
# Skills
if name == "Health Up":
quantity = self.get_setting("health_pool")
quantity = self.options.health_pool.value
elif name == "Mana Up":
quantity = self.get_setting("mana_pool")
quantity = self.options.mana_pool.value
elif name == "Attack Up":
quantity = self.get_setting("attack_pool")
quantity = self.options.attack_pool.value
elif name == "Magic Damage Up":
quantity = self.get_setting("magic_damage_pool")
quantity = self.options.magic_damage_pool.value
elif name == "Armor Up":
quantity = self.get_setting("armor_pool")
quantity = self.options.armor_pool.value
elif name == "Equip Up":
quantity = self.get_setting("equip_pool")
quantity = self.options.equip_pool.value
elif name == "Crit Chance Up":
quantity = self.get_setting("crit_chance_pool")
quantity = self.options.crit_chance_pool.value
elif name == "Crit Damage Up":
quantity = self.get_setting("crit_damage_pool")
quantity = self.options.crit_damage_pool.value
# Ignore filler, it will be added in a later stage.
if data.category == "Filler":
@@ -215,7 +212,7 @@ class RLWorld(World):
def get_filler_item_name(self) -> str:
fillers = get_items_by_category("Filler")
weights = [data.weight for data in fillers.values()]
return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0]
def create_item(self, name: str) -> RLItem:
data = item_table[name]
@@ -226,10 +223,10 @@ class RLWorld(World):
return RLItem(name, data.classification, data.code, self.player)
def set_rules(self):
set_rules(self.multiworld, self.player)
set_rules(self, self.player)
def create_regions(self):
create_regions(self.multiworld, self.player)
create_regions(self)
self._place_events()
def _place_events(self):
@@ -238,7 +235,7 @@ class RLWorld(World):
self.create_event("Defeat The Fountain"))
# Khidr / Neo Khidr
if self.get_setting("khidr") == "vanilla":
if self.options.khidr == "vanilla":
self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item(
self.create_event("Defeat Khidr"))
else:
@@ -246,7 +243,7 @@ class RLWorld(World):
self.create_event("Defeat Neo Khidr"))
# Alexander / Alexander IV
if self.get_setting("alexander") == "vanilla":
if self.options.alexander == "vanilla":
self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item(
self.create_event("Defeat Alexander"))
else:
@@ -254,7 +251,7 @@ class RLWorld(World):
self.create_event("Defeat Alexander IV"))
# Ponce de Leon / Ponce de Freon
if self.get_setting("leon") == "vanilla":
if self.options.leon == "vanilla":
self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item(
self.create_event("Defeat Ponce de Leon"))
else:
@@ -262,7 +259,7 @@ class RLWorld(World):
self.create_event("Defeat Ponce de Freon"))
# Herodotus / Astrodotus
if self.get_setting("herodotus") == "vanilla":
if self.options.herodotus == "vanilla":
self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item(
self.create_event("Defeat Herodotus"))
else:

View File

@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
class RLTestBase(WorldTestBase):

View File

@@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int):
regBitS.subregions = [bits_top]
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None):
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance:
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
sourceRegion.connect(targetRegion, rule=rule)
return sourceRegion.connect(targetRegion, rule=rule)
def create_region(name: str, player: int, world: MultiWorld) -> SM64Region:

View File

@@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict,
connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"],
rf.build_rule("GP"))
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
# Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that
# needs to be registered is its parent region.
world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance)
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))

View File

@@ -15,13 +15,13 @@ from .. import options
from ..data.harvest import HarvestCropSource
from ..mods.logic.magic_logic import MagicLogicMixin
from ..mods.logic.mod_skills_levels import get_mod_skill_levels
from ..stardew_rule import StardewRule, True_, False_, true_, And
from ..stardew_rule import StardewRule, true_, True_, False_
from ..strings.craftable_names import Fishing
from ..strings.machine_names import Machine
from ..strings.performance_names import Performance
from ..strings.quality_names import ForageQuality
from ..strings.region_names import Region
from ..strings.skill_names import Skill, all_mod_skills
from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills
from ..strings.tool_names import ToolMaterial, Tool
from ..strings.wallet_item_names import Wallet
@@ -43,22 +43,17 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
if level <= 0:
return True_()
tool_level = (level - 1) // 2
tool_level = min(4, (level - 1) // 2)
tool_material = ToolMaterial.tiers[tool_level]
months = max(1, level - 1)
months_rule = self.logic.time.has_lived_months(months)
if self.options.skill_progression != options.SkillProgression.option_vanilla:
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
else:
previous_level_rule = true_
previous_level_rule = self.logic.skill.has_previous_level(skill, level)
if skill == Skill.fishing:
xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3))
elif skill == Skill.farming:
xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
elif skill == Skill.foraging:
xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\
xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) | \
self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
elif skill == Skill.mining:
xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \
@@ -70,22 +65,34 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
elif skill in all_mod_skills:
# Ideal solution would be to add a logic registry, but I'm too lazy.
return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
else:
raise Exception(f"Unknown skill: {skill}")
return previous_level_rule & months_rule & xp_rule
return previous_level_rule & xp_rule
# Should be cached
def has_level(self, skill: str, level: int) -> StardewRule:
if level <= 0:
return True_()
assert level >= 0, f"There is no level before level 0."
if level == 0:
return true_
if self.options.skill_progression == options.SkillProgression.option_vanilla:
return self.logic.skill.can_earn_level(skill, level)
return self.logic.received(f"{skill} Level", level)
def has_previous_level(self, skill: str, level: int) -> StardewRule:
assert level > 0, f"There is no level before level 0."
if level == 1:
return true_
if self.options.skill_progression == options.SkillProgression.option_vanilla:
months = max(1, level - 1)
return self.logic.time.has_lived_months(months)
return self.logic.received(f"{skill} Level", level - 1)
@cache_self1
def has_farming_level(self, level: int) -> StardewRule:
return self.logic.skill.has_level(Skill.farming, level)
@@ -108,18 +115,9 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
return rule_with_fishing
return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing
def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_vanilla:
return self.has_total_level(50)
skills_items = vanilla_skill_items
if included_modded_skills:
skills_items += get_mod_skill_levels(self.options.mods)
return And(*[self.logic.received(skill, 10) for skill in skills_items])
def can_enter_mastery_cave(self) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
return self.logic.received(Wallet.mastery_of_the_five_ways)
return self.has_all_skills_maxed()
def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule:
skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills)
return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills))
@cached_property
def can_get_farming_xp(self) -> StardewRule:
@@ -197,13 +195,19 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
return self.has_level(Skill.foraging, 9)
return False_()
@cached_property
def can_earn_mastery_experience(self) -> StardewRule:
if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months
return self.logic.time.has_lived_max_months
def can_earn_mastery(self, skill: str) -> StardewRule:
# Checking for level 11, so it includes having level 10 and being able to earn xp.
return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave)
def has_mastery(self, skill: str) -> StardewRule:
if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries:
return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave)
return self.logic.received(f"{skill} Mastery")
if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
return self.logic.received(f"{skill} Mastery")
return self.logic.skill.can_earn_mastery(skill)
@cached_property
def can_enter_mastery_cave(self) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries:
return self.logic.received(Wallet.mastery_of_the_five_ways)
return self.has_any_skills_maxed(included_modded_skills=False)

View File

@@ -154,7 +154,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw
extra_raccoons = extra_raccoons + num
bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules
if num > 1:
previous_bundle_name = f"Raccoon Request {num-1}"
previous_bundle_name = f"Raccoon Request {num - 1}"
bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name)
room_rules.append(bundle_rules)
MultiWorldRules.set_rule(location, bundle_rules)
@@ -168,13 +168,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta
mods = world_options.mods
if world_options.skill_progression == SkillProgression.option_vanilla:
return
for i in range(1, 11):
set_vanilla_skill_rule_for_level(logic, multiworld, player, i)
set_modded_skill_rule_for_level(logic, multiworld, player, mods, i)
if world_options.skill_progression != SkillProgression.option_progressive_with_masteries:
if world_options.skill_progression == SkillProgression.option_progressive:
return
for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]:
MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience)
MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill))
def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int):
@@ -256,8 +259,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen)
set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin)
set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce))
set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave())
set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave)
set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2))
set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two)
set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three)

View File

@@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x():
options.QuestLocations.internal_name: 56,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized,
options.Shipsanity.internal_name: options.Shipsanity.option_everything,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi,
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
options.TrapItems.internal_name: options.TrapItems.option_nightmare,
@@ -310,6 +310,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
self.multiworld.worlds[self.player].total_progression_items -= 1
return created_item
def remove_one_by_name(self, item: str) -> None:
self.remove(self.create_item(item))
def reset_collection_state(self):
self.multiworld.state = self.original_state.copy()
pre_generated_worlds = {}

View File

@@ -1,23 +1,30 @@
from ... import HasProgressionPercent
from ... import HasProgressionPercent, StardewLogic
from ...options import ToolProgression, SkillProgression, Mods
from ...strings.skill_names import all_skills
from ...strings.skill_names import all_skills, all_vanilla_skills, Skill
from ...test import SVTestBase
class TestVanillaSkillLogicSimplification(SVTestBase):
class TestSkillProgressionVanilla(SVTestBase):
options = {
SkillProgression.internal_name: SkillProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_progressive,
}
def test_skill_logic_has_level_only_uses_one_has_progression_percent(self):
rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8)
self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent))
rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8)
self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent))
def test_has_mastery_requires_month_equivalent_to_10_levels(self):
logic: StardewLogic = self.multiworld.worlds[1].logic
rule = logic.skill.has_mastery(Skill.farming)
time_rule = logic.time.has_lived_months(10)
self.assertIn(time_rule, rule.current_rules)
class TestAllSkillsRequirePrevious(SVTestBase):
class TestSkillProgressionProgressive(SVTestBase):
options = {
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
SkillProgression.internal_name: SkillProgression.option_progressive,
Mods.internal_name: frozenset(Mods.valid_keys),
}
@@ -25,16 +32,82 @@ class TestAllSkillsRequirePrevious(SVTestBase):
for skill in all_skills:
self.collect_everything()
self.remove_by_name(f"{skill} Level")
for level in range(1, 11):
location_name = f"Level {level} {skill}"
location = self.multiworld.get_location(location_name, self.player)
with self.subTest(location_name):
can_reach = self.can_reach_location(location_name)
if level > 1:
self.assertFalse(can_reach)
self.assert_reach_location_false(location, self.multiworld.state)
self.collect(f"{skill} Level")
can_reach = self.can_reach_location(location_name)
self.assertTrue(can_reach)
self.multiworld.state = self.original_state.copy()
self.assert_reach_location_true(location, self.multiworld.state)
self.reset_collection_state()
def test_has_level_requires_exact_amount_of_levels(self):
logic: StardewLogic = self.multiworld.worlds[1].logic
rule = logic.skill.has_level(Skill.farming, 8)
level_rule = logic.received("Farming Level", 8)
self.assertEqual(level_rule, rule)
def test_has_previous_level_requires_one_less_level_than_requested(self):
logic: StardewLogic = self.multiworld.worlds[1].logic
rule = logic.skill.has_previous_level(Skill.farming, 8)
level_rule = logic.received("Farming Level", 7)
self.assertEqual(level_rule, rule)
def test_has_mastery_requires_10_levels(self):
logic: StardewLogic = self.multiworld.worlds[1].logic
rule = logic.skill.has_mastery(Skill.farming)
level_rule = logic.received("Farming Level", 10)
self.assertIn(level_rule, rule.current_rules)
class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase):
options = {
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
ToolProgression.internal_name: ToolProgression.option_progressive,
Mods.internal_name: frozenset(),
}
def test_has_mastery_requires_the_item(self):
logic: StardewLogic = self.multiworld.worlds[1].logic
rule = logic.skill.has_mastery(Skill.farming)
received_mastery = logic.received("Farming Mastery")
self.assertEqual(received_mastery, rule)
def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self):
self.collect_everything()
for skill in all_vanilla_skills:
with self.subTest(skill):
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_reach_location_true(location, self.multiworld.state)
self.reset_collection_state()
def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self):
for skill in all_vanilla_skills:
with self.subTest(skill):
self.collect_everything()
self.remove_one_by_name(f"{skill} Level")
location = self.multiworld.get_location(f"{skill} Mastery", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.reset_collection_state()
def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self):
self.collect_everything()
self.remove_one_by_name(f"Progressive Pickaxe")
location = self.multiworld.get_location("Mining Mastery", self.player)
self.assert_reach_location_false(location, self.multiworld.state)
self.reset_collection_state()

View File

@@ -61,7 +61,7 @@ class WitnessWorld(World):
item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
required_client_version = (0, 4, 5)
required_client_version = (0, 5, 1)
player_logic: WitnessPlayerLogic
player_locations: WitnessPlayerLocations

View File

@@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
location_name = hint.location.name
if hint.location.player != world.player:
location_name += " (" + world.player_name + ")"
location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
item = hint.location.item
@@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
item_name = item.name
if item.player != world.player:
item_name += " (" + world.player_name + ")"
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
hint_text = ""
area: Optional[str] = None

View File

@@ -466,6 +466,9 @@ class YachtDiceWorld(World):
menu.exits.append(connection)
connection.connect(board)
self.multiworld.regions += [menu, board]
def get_filler_item_name(self) -> str:
return "Good RNG"
def set_rules(self):
"""