mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 07:33:20 -07:00
Merge branch 'main' into tunc-portal-direction-pairing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
14
Options.py
14
Options.py
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)])
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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}/")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class RLTestBase(WorldTestBase):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user