diff --git a/AdventureClient.py b/AdventureClient.py index 06e4d60dad..7bfbd5ef6b 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -112,7 +112,7 @@ class AdventureContext(CommonContext): if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" + msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: diff --git a/BaseClasses.py b/BaseClasses.py index ada18f1e1d..88857f8032 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -728,7 +728,7 @@ class CollectionState(): return True return False - def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool: + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" found: int = 0 @@ -743,7 +743,7 @@ class CollectionState(): """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int: + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -758,7 +758,7 @@ class CollectionState(): return True return False - def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool: + def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool: """Returns True if the state contains at least `count` items present in a specified item group. Ignores duplicates of the same item. """ @@ -778,7 +778,7 @@ class CollectionState(): for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group] ) - def count_group_exclusive(self, item_name_group: str, player: int) -> int: + def count_group_unique(self, item_name_group: str, player: int) -> int: """Returns the cumulative count of items from an item group present in state. Ignores duplicates of the same item.""" player_prog_items = self.prog_items[player] diff --git a/CommonClient.py b/CommonClient.py index 63cac098e2..8af822cba5 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import logging import asyncio @@ -8,6 +9,7 @@ import sys import typing import time import functools +import warnings import ModuleUpdate ModuleUpdate.update() @@ -173,10 +175,74 @@ class CommonContext: items_handling: typing.Optional[int] = None want_slot_data: bool = True # should slot_data be retrieved via Connect - # data package - # Contents in flux until connection to server is made, to download correct data for this multiworld. - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + class NameLookupDict: + """A specialized dict, with helper methods, for id -> name item/location data package lookups by game.""" + def __init__(self, ctx: CommonContext, lookup_type: typing.Literal["item", "location"]): + self.ctx: CommonContext = ctx + self.lookup_type: typing.Literal["item", "location"] = lookup_type + self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})" + self._archipelago_lookup: typing.Dict[int, str] = {} + self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item) + self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict( + lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item))) + self.warned: bool = False + + # noinspection PyTypeChecker + def __getitem__(self, key: str) -> typing.Mapping[int, str]: + # TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support. + if isinstance(key, int): + if not self.warned: + # Use warnings instead of logger to avoid deprecation message from appearing on user side. + self.warned = True + warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain " + f"backwards compatibility for now. If multiple games share the same id for a " + f"{self.lookup_type}, name could be incorrect. Please use " + f"`{self.lookup_type}_names.lookup_in_game()` or " + f"`{self.lookup_type}_names.lookup_in_slot()` instead.") + return self._flat_store[key] # type: ignore + + return self._game_store[key] + + def __len__(self) -> int: + return len(self._game_store) + + def __iter__(self) -> typing.Iterator[str]: + return iter(self._game_store) + + def __repr__(self) -> str: + return self._game_store.__repr__() + + def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str: + """Returns the name for an item/location id in the context of a specific game or own game if `game` is + omitted. + """ + if game_name is None: + game_name = self.ctx.game + assert game_name is not None, f"Attempted to lookup {self.lookup_type} with no game name available." + + return self._game_store[game_name][code] + + def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str: + """Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is + omitted. + """ + if slot is None: + slot = self.ctx.slot + assert slot is not None, f"Attempted to lookup {self.lookup_type} with no slot info available." + + return self.lookup_in_game(code, self.ctx.slot_info[slot].game) + + def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) -> None: + """Overrides existing lookup tables for a particular game.""" + id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item) + id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()}) + self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table) + self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method. + if game == "Archipelago": + # Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage, + # it updates in all chain maps automatically. + self._archipelago_lookup.clear() + self._archipelago_lookup.update(id_to_name_lookup_table) # defaults starting_reconnect_delay: int = 5 @@ -231,7 +297,7 @@ class CommonContext: # message box reporting a loss of connection _messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None - def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None: # server state self.server_address = server_address self.username = None @@ -271,6 +337,9 @@ class CommonContext: self.exit_event = asyncio.Event() self.watcher_event = asyncio.Event() + self.item_names = self.NameLookupDict(self, "item") + self.location_names = self.NameLookupDict(self, "location") + self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) self.update_data_package(network_data_package) @@ -486,19 +555,17 @@ class CommonContext: or remote_checksum != cache_checksum: needed_updates.add(game) else: - self.update_game(cached_game) + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) - def update_game(self, game_package: dict): - for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name - for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + def update_game(self, game_package: dict, game: str): + self.item_names.update_game(game, game_package["item_name_to_id"]) + self.location_names.update_game(game, game_package["location_name_to_id"]) def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): - self.update_game(game_data) + self.update_game(game_data, game) def consume_network_data_package(self, data_package: dict): self.update_data_package(data_package) diff --git a/Fill.py b/Fill.py index d9919c1338..d8147b2eac 100644 --- a/Fill.py +++ b/Fill.py @@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. - :param locations: Locations to be filled with item_pool - :param item_pool: Items to fill into the locations + :param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled. + :param item_pool: Items to fill into the locations, gets mutated by removing items that get placed. :param single_player_placement: if true, can speed up placement if everything belongs to a single player :param lock: locations are set to locked as they are filled :param swap: if true, swaps of already place items are done in the event of a dead end @@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item], - name: str = "Remaining") -> None: + name: str = "Remaining", + move_unplaceable_to_start_inventory: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() @@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld, if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them - raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" - f"Unplaced items:\n" - f"{', '.join(str(item) for item in unplaced_items)}\n" - f"Unfilled locations:\n" - f"{', '.join(str(location) for location in locations)}\n" - f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + if move_unplaceable_to_start_inventory: + last_batch = [] + for item in unplaced_items: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + last_batch.append(multiworld.worlds[item.player].create_filler()) + remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry") + else: + raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" + f"Unplaced items:\n" + f"{', '.join(str(item) for item in unplaced_items)}\n" + f"Unfilled locations:\n" + f"{', '.join(str(location) for location in locations)}\n" + f"Already placed {len(placements)}:\n" + f"{', '.join(str(place) for place in placements)}") itempool.extend(unplaced_items) @@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld, return fill_locations, itempool -def distribute_items_restrictive(multiworld: MultiWorld) -> None: +def distribute_items_restrictive(multiworld: MultiWorld, + panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None: fill_locations = sorted(multiworld.get_unfilled_locations()) multiworld.random.shuffle(fill_locations) # get items to distribute @@ -470,8 +480,29 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None: if progitempool: # "advancement/progression fill" - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1, - name="Progression") + if panic_method == "swap": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "raise": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "start_inventory": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, allow_partial=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + if progitempool: + for item in progitempool: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + filleritempool.append(multiworld.worlds[item.player].create_filler()) + logging.warning(f"{len(progitempool)} items moved to start inventory," + f" due to failure in Progression fill step.") + progitempool[:] = [] + + else: + raise ValueError(f"Generator Panic Method {panic_method} not recognized.") if progitempool: raise FillError( f"Not enough locations for progression items. " @@ -486,7 +517,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None: inaccessible_location_rules(multiworld, multiworld.state, defaultlocations) - remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded") + remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded", + move_unplaceable_to_start_inventory=panic_method=="start_inventory") + if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " @@ -495,7 +528,8 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None: restitempool = filleritempool + usefulitempool - remaining_fill(multiworld, defaultlocations, restitempool) + remaining_fill(multiworld, defaultlocations, restitempool, + move_unplaceable_to_start_inventory=panic_method=="start_inventory") unplaced = restitempool unfilled = defaultlocations diff --git a/Generate.py b/Generate.py index 2bc061b746..67988bf8b3 100644 --- a/Generate.py +++ b/Generate.py @@ -9,6 +9,7 @@ import urllib.parse import urllib.request from collections import Counter from typing import Any, Dict, Tuple, Union +from itertools import chain import ModuleUpdate @@ -22,9 +23,7 @@ from Main import main as ERmain from settings import get_settings from Utils import parse_yamls, version_tuple, __version__, tuplize_version from worlds.alttp.EntranceRandomizer import parse_arguments -from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister -from worlds.generic import PlandoConnection from worlds import failed_world_loads @@ -319,18 +318,34 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str logging.debug(f'Applying {new_weights}') cleaned_weights = {} for option in new_weights: - option_name = option.lstrip("+") + option_name = option.lstrip("+-") if option.startswith("+") and option_name in weights: cleaned_value = weights[option_name] new_value = new_weights[option] - if isinstance(new_value, (set, dict)): + if isinstance(new_value, set): cleaned_value.update(new_value) elif isinstance(new_value, list): cleaned_value.extend(new_value) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) + Counter(new_value)) else: raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name}," f" received {type(new_value).__name__}.") cleaned_weights[option_name] = cleaned_value + elif option.startswith("-") and option_name in weights: + cleaned_value = weights[option_name] + new_value = new_weights[option] + if isinstance(new_value, set): + cleaned_value.difference_update(new_value) + elif isinstance(new_value, list): + for element in new_value: + cleaned_value.remove(element) + elif isinstance(new_value, dict): + cleaned_value = dict(Counter(cleaned_value) - Counter(new_value)) + else: + raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name}," + f" received {type(new_value).__name__}.") + cleaned_weights[option_name] = cleaned_value else: cleaned_weights[option_name] = new_weights[option] new_options = set(cleaned_weights) - set(weights) @@ -415,7 +430,6 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, player_option = option.from_any(game_weights[option_key]) else: player_option = option.from_any(get_choice(option_key, game_weights)) - del game_weights[option_key] else: player_option = option.from_any(option.default) # call the from_any here to support default "random" setattr(ret, option_key, player_option) @@ -429,9 +443,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if "linked_options" in weights: weights = roll_linked_options(weights) - valid_trigger_names = set() + valid_keys = set() if "triggers" in weights: - weights = roll_triggers(weights, weights["triggers"], valid_trigger_names) + weights = roll_triggers(weights, weights["triggers"], valid_keys) requirements = weights.get("requires", {}) if requirements: @@ -466,12 +480,14 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b world_type = AutoWorldRegister.world_types[ret.game] game_weights = weights[ret.game] - if any(weight.startswith("+") for weight in game_weights) or \ - any(weight.startswith("+") for weight in weights): - raise Exception(f"Merge tag cannot be used outside of trigger contexts.") + for weight in chain(game_weights, weights): + if weight.startswith("+"): + raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}") + if weight.startswith("-"): + raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}") if "triggers" in game_weights: - weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names) + weights = roll_triggers(weights, game_weights["triggers"], valid_keys) game_weights = weights[ret.game] ret.name = get_choice('name', weights) @@ -480,42 +496,20 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) + valid_keys.add(option_key) for option_key in game_weights: - if option_key in {"triggers", *valid_trigger_names}: + if option_key in {"triggers", *valid_keys}: continue logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "A Link to the Past": - roll_alttp_settings(ret, game_weights, plando_options) - if PlandoOptions.connections in plando_options: - ret.plando_connections = [] - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement, "both") - )) + roll_alttp_settings(ret, game_weights) return ret -def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): - - ret.plando_texts = {} - if PlandoOptions.texts in plando_options: - tt = TextTable() - tt.removeUnwantedText() - options = weights.get("plando_texts", []) - for placement in options: - if roll_percentage(get_choice_legacy("percentage", placement, 100)): - at = str(get_choice_legacy("at", placement)) - if at not in tt: - raise Exception(f"No text target \"{at}\" found.") - ret.plando_texts[at] = str(get_choice_legacy("text", placement)) - +def roll_alttp_settings(ret: argparse.Namespace, weights): ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Main.py b/Main.py index 1be91a8bb2..de6b467f93 100644 --- a/Main.py +++ b/Main.py @@ -13,7 +13,7 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items from Options import StartInventoryPool -from Utils import __version__, output_path, version_tuple +from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -272,7 +272,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if multiworld.algorithm == 'flood': flood_items(multiworld) # different algo, biased towards early game progress items elif multiworld.algorithm == 'balanced': - distribute_items_restrictive(multiworld) + distribute_items_restrictive(multiworld, get_settings().generator.panic_method) AutoWorld.call_all(multiworld, 'post_fill') @@ -372,6 +372,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} + # get spheres -> filter address==None -> skip empty + spheres: List[Dict[int, Set[int]]] = [] + for sphere in multiworld.get_spheres(): + current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) + for sphere_location in sphere: + if type(sphere_location.address) is int: + current_sphere[sphere_location.player].add(sphere_location.address) + + if current_sphere: + spheres.append(dict(current_sphere)) + multidata = { "slot_data": slot_data, "slot_info": slot_info, @@ -386,6 +397,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": multiworld.seed_name, + "spheres": spheres, "datapackage": data_package, } AutoWorld.call_all(multiworld, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index e95e44dd7d..22375da2b3 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -37,7 +37,7 @@ except ImportError: import NetUtils import Utils -from Utils import version_tuple, restricted_loads, Version, async_start +from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType, LocationStore @@ -168,15 +168,20 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + item_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') + location_names: typing.Dict[str, typing.Dict[int, str]] = ( + collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]] + spheres: typing.List[typing.Dict[int, typing.Set[int]]] + """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger + def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -238,6 +243,7 @@ class Context: self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) self.read_data = {} + self.spheres = [] # init empty to satisfy linter, I suppose self.gamespackage = {} @@ -267,14 +273,21 @@ class Context: if "checksum" in game_package: self.checksums[game_name] = game_package["checksum"] for item_name, item_id in game_package["item_name_to_id"].items(): - self.item_names[item_id] = item_name + self.item_names[game_name][item_id] = item_name for location_name, location_id in game_package["location_name_to_id"].items(): - self.location_names[location_id] = location_name + self.location_names[game_name][location_id] = location_name self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) self.all_location_and_group_names[game_name] = \ set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) + archipelago_item_names = self.item_names["Archipelago"] + archipelago_location_names = self.location_names["Archipelago"] + for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]: + # Add Archipelago items and locations to each data package. + self.item_names[game].update(archipelago_item_names) + self.location_names[game].update(archipelago_location_names) + def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None @@ -466,6 +479,9 @@ class Context: for game_name, data in self.location_name_groups.items(): self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] + # sorted access spheres + self.spheres = decoded_obj.get("spheres", []) + # saving def save(self, now=False) -> bool: @@ -624,6 +640,16 @@ class Context: self.recheck_hints(team, slot) return self.hints[team, slot] + def get_sphere(self, player: int, location_id: int) -> int: + """Get sphere of a location, -1 if spheres are not available.""" + if self.spheres: + for i, sphere in enumerate(self.spheres): + if location_id in sphere.get(player, set()): + return i + raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. " + f"Location or player may not exist.") + return -1 + def get_players_package(self): return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] @@ -766,10 +792,7 @@ async def on_client_connected(ctx: Context, client: Client): for slot, connected_clients in clients.items(): if connected_clients: name = ctx.player_names[team, slot] - players.append( - NetworkPlayer(team, slot, - ctx.name_aliases.get((team, slot), name), name) - ) + players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name)) games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} games.add("Archipelago") await ctx.send_msgs(client, [{ @@ -784,8 +807,6 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_versions': {game: game_data["version"] for game, game_data - in ctx.gamespackage.items() if game in games}, 'datapackage_checksums': {game: game_data["checksum"] for game, game_data in ctx.gamespackage.items() if game in games and "checksum" in game_data}, 'seed_name': ctx.seed_name, @@ -989,8 +1010,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], - ctx.player_names[(team, target_player)], ctx.location_names[location])) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], + ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -1044,8 +1065,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{ctx.item_names[hint.item]} is " \ - f"at {ctx.location_names[hint.location]} " \ + f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ + f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1074,28 +1095,6 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int): "item": net_item} -def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: - picks = Utils.get_fuzzy_results(input_text, possible_answers, limit=2) - if len(picks) > 1: - dif = picks[0][1] - picks[1][1] - if picks[0][1] == 100: - return picks[0][0], True, "Perfect Match" - elif picks[0][1] < 75: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - elif dif > 5: - return picks[0][0], True, "Close Match" - else: - return picks[0][0], False, f"Too many close matches for '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - else: - if picks[0][1] > 90: - return picks[0][0], True, "Only Option Match" - else: - return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ - f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" - - class CommandMeta(type): def __new__(cls, name, bases, attrs): commands = attrs["commands"] = {} @@ -1347,7 +1346,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1360,7 +1359,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1378,7 +1377,8 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1403,7 +1403,8 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - names = [self.ctx.location_names[location] for location in locations] + game = self.ctx.slot_info[self.client.slot].game + names = [self.ctx.location_names[game][location] for location in locations] if filter_text: location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] if filter_text in location_groups: # location group name @@ -1484,10 +1485,10 @@ class ClientMessageProcessor(CommonCommandProcessor): elif input_text.isnumeric(): game = self.ctx.games[self.client.slot] hint_id = int(input_text) - hint_name = self.ctx.item_names[hint_id] \ - if not for_location and hint_id in self.ctx.item_names \ - else self.ctx.location_names[hint_id] \ - if for_location and hint_id in self.ctx.location_names \ + hint_name = self.ctx.item_names[game][hint_id] \ + if not for_location and hint_id in self.ctx.item_names[game] \ + else self.ctx.location_names[game][hint_id] \ + if for_location and hint_id in self.ctx.location_names[game] \ else None if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") @@ -1549,6 +1550,9 @@ class ClientMessageProcessor(CommonCommandProcessor): self.ctx.random.shuffle(not_found_hints) # By popular vote, make hints prefer non-local placements not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) + # By another popular vote, prefer early sphere + not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location), + reverse=True) hints = found_hints + old_hints while can_pay > 0: @@ -1558,10 +1562,10 @@ class ClientMessageProcessor(CommonCommandProcessor): hints.append(hint) can_pay -= 1 self.ctx.hints_used[self.client.team, self.client.slot] += 1 - points_available = get_client_points(self.ctx, self.client) self.ctx.notify_hints(self.client.team, hints) if not_found_hints: + points_available = get_client_points(self.ctx, self.client) if hints and cost and int((points_available // cost) == 0): self.output( f"There may be more hintables, however, you cannot afford to pay for any more. " diff --git a/NetUtils.py b/NetUtils.py index 8fc3929e60..076fdc3ba4 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -247,7 +247,7 @@ class JSONtoTextParser(metaclass=HandlerMeta): def _handle_item_id(self, node: JSONMessagePart): item_id = int(node["text"]) - node["text"] = self.ctx.item_names[item_id] + node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"]) return self._handle_item_name(node) def _handle_location_name(self, node: JSONMessagePart): @@ -255,8 +255,8 @@ class JSONtoTextParser(metaclass=HandlerMeta): return self._handle_color(node) def _handle_location_id(self, node: JSONMessagePart): - item_id = int(node["text"]) - node["text"] = self.ctx.location_names[item_id] + location_id = int(node["text"]) + node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"]) return self._handle_location_name(node) def _handle_entrance_name(self, node: JSONMessagePart): diff --git a/Options.py b/Options.py index 39fd567656..40a6996d32 100644 --- a/Options.py +++ b/Options.py @@ -12,6 +12,7 @@ from copy import deepcopy from dataclasses import dataclass from schema import And, Optional, Or, Schema +from typing_extensions import Self from Utils import get_fuzzy_results, is_iterable_except_str @@ -896,6 +897,228 @@ class ItemSet(OptionSet): convert_name_groups = True +class PlandoText(typing.NamedTuple): + at: str + text: typing.List[str] + percentage: int = 100 + + +PlandoTextsFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any +] + + +class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): + default = () + supports_weighting = False + display_name = "Plando Texts" + + def __init__(self, value: typing.Iterable[PlandoText]) -> None: + self.value = list(deepcopy(value)) + super().__init__() + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.texts & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando texts module is turned off, " + f"so text for {player_name} will be ignored.") + + @classmethod + def from_any(cls, data: PlandoTextsFromAnyType) -> Self: + texts: typing.List[PlandoText] = [] + if isinstance(data, typing.Iterable): + for text in data: + if isinstance(text, typing.Mapping): + if random.random() < float(text.get("percentage", 100)/100): + at = text.get("at", None) + if at is not None: + given_text = text.get("text", []) + if isinstance(given_text, str): + given_text = [given_text] + texts.append(PlandoText( + at, + given_text, + text.get("percentage", 100) + )) + elif isinstance(text, PlandoText): + if random.random() < float(text.percentage/100): + texts.append(text) + else: + raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") + cls.verify_keys([text.at for text in texts]) + return cls(texts) + else: + raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoText]) -> str: + return str({text.at: " ".join(text.text) for text in value}) + + def __iter__(self) -> typing.Iterator[PlandoText]: + yield from self.value + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoText: + return self.value.__getitem__(index) + + def __len__(self) -> int: + return self.value.__len__() + + +class ConnectionsMeta(AssembleOptions): + def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]): + if name != "PlandoConnections": + assert "entrances" in attrs, f"Please define valid entrances for {name}" + attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) + assert "exits" in attrs, f"Please define valid exits for {name}" + attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + if "__doc__" not in attrs: + attrs["__doc__"] = PlandoConnections.__doc__ + cls = super().__new__(mcs, name, bases, attrs) + return cls + + +class PlandoConnection(typing.NamedTuple): + class Direction: + entrance = "entrance" + exit = "exit" + both = "both" + + entrance: str + exit: str + direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped + percentage: int = 100 + + +PlandoConFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any +] + + +class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta): + """Generic connections plando. Format is: + - entrance: "Entrance Name" + exit: "Exit Name" + direction: "Direction" + percentage: 100 + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + + display_name = "Plando Connections" + + default = () + supports_weighting = False + + entrances: typing.ClassVar[typing.AbstractSet[str]] + exits: typing.ClassVar[typing.AbstractSet[str]] + + duplicate_exits: bool = False + """Whether or not exits should be allowed to be duplicate.""" + + def __init__(self, value: typing.Iterable[PlandoConnection]): + self.value = list(deepcopy(value)) + super(PlandoConnections, self).__init__() + + @classmethod + def validate_entrance_name(cls, entrance: str) -> bool: + return entrance.lower() in cls.entrances + + @classmethod + def validate_exit_name(cls, exit: str) -> bool: + return exit.lower() in cls.exits + + @classmethod + def can_connect(cls, entrance: str, exit: str) -> bool: + """Checks that a given entrance can connect to a given exit. + By default, this will always return true unless overridden.""" + return True + + @classmethod + def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None: + used_entrances: typing.List[str] = [] + used_exits: typing.List[str] = [] + for connection in connections: + entrance = connection.entrance + exit = connection.exit + direction = connection.direction + if direction not in (PlandoConnection.Direction.entrance, + PlandoConnection.Direction.exit, + PlandoConnection.Direction.both): + raise ValueError(f"Unknown direction: {direction}") + if entrance in used_entrances: + raise ValueError(f"Duplicate Entrance {entrance} not allowed.") + if not cls.duplicate_exits and exit in used_exits: + raise ValueError(f"Duplicate Exit {exit} not allowed.") + used_entrances.append(entrance) + used_exits.append(exit) + if not cls.validate_entrance_name(entrance): + raise ValueError(f"{entrance.title()} is not a valid entrance.") + if not cls.validate_exit_name(exit): + raise ValueError(f"{exit.title()} is not a valid exit.") + if not cls.can_connect(entrance, exit): + raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + + @classmethod + def from_any(cls, data: PlandoConFromAnyType) -> Self: + if not isinstance(data, typing.Iterable): + raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.") + + value: typing.List[PlandoConnection] = [] + for connection in data: + if isinstance(connection, typing.Mapping): + percentage = connection.get("percentage", 100) + if random.random() < float(percentage / 100): + entrance = connection.get("entrance", None) + if is_iterable_except_str(entrance): + entrance = random.choice(sorted(entrance)) + exit = connection.get("exit", None) + if is_iterable_except_str(exit): + exit = random.choice(sorted(exit)) + direction = connection.get("direction", "both") + + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction, + percentage + )) + elif isinstance(connection, PlandoConnection): + if random.random() < float(connection.percentage / 100): + value.append(connection) + else: + raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") + cls.validate_plando_connections(value) + return cls(value) + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if self.value and not (PlandoOptions.connections & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando connections module is turned off, " + f"so connections for {player_name} will be ignored.") + + @classmethod + def get_option_name(cls, value: typing.List[PlandoConnection]) -> str: + return ", ".join(["%s %s %s" % (connection.entrance, + "<=>" if connection.direction == PlandoConnection.Direction.both else + "<=" if connection.direction == PlandoConnection.Direction.exit else + "=>", + connection.exit) for connection in value]) + + def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection: + return self.value.__getitem__(index) + + def __iter__(self) -> typing.Iterator[PlandoConnection]: + yield from self.value + + def __len__(self) -> int: + return len(self.value) + + class Accessibility(Choice): """Set rules for reachability of your items/locations. Locations: ensure everything can be reached and acquired. @@ -910,8 +1133,10 @@ class Accessibility(Choice): class ProgressionBalancing(NamedRange): - """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. - A lower setting means more getting stuck. A higher setting means less getting stuck.""" + """ + A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. + A lower setting means more getting stuck. A higher setting means less getting stuck. + """ default = 50 range_start = 0 range_end = 99 @@ -984,7 +1209,7 @@ class LocalItems(ItemSet): class NonLocalItems(ItemSet): """Forces these items to be outside their native world.""" - display_name = "Not Local Items" + display_name = "Non-local Items" class StartInventory(ItemDict): @@ -1047,7 +1272,8 @@ class ItemLinks(OptionList): ]) @staticmethod - def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set: + def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, + allow_item_groups: bool = True) -> typing.Set: pool = set() for item_name in items: if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups): @@ -1130,9 +1356,41 @@ class OptionGroup(typing.NamedTuple): """Name of the group to categorize these options in for display on the WebHost and in generated YAMLS.""" options: typing.List[typing.Type[Option[typing.Any]]] """Options to be in the defined group.""" + start_collapsed: bool = False + """Whether the group will start collapsed on the WebHost options pages.""" -def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): +item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, + StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] +""" +Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group. +If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to +it. +""" + + +def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ + str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: + """Generates and returns a dictionary for the option groups of a specified world.""" + option_groups = {option: option_group.name + for option_group in world.web.option_groups + for option in option_group.options} + # add a default option group for uncategorized options to get thrown into + ordered_groups = ["Game Options"] + [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] + grouped_options = {group: {} for group in ordered_groups} + for option_name, option in world.options_dataclass.type_hints.items(): + if visibility_level & option.visibility: + grouped_options[option_groups.get(option, "Game Options")][option_name] = option + + # if the world doesn't have any ungrouped options, this group will be empty so just remove it + if not grouped_options["Game Options"]: + del grouped_options["Game Options"] + + return grouped_options + + +def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: import os import yaml @@ -1170,17 +1428,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if option.visibility >= Visibility.template: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option - + grouped_options = get_option_groups(world) with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( diff --git a/README.md b/README.md index 4633c99c66..cebd4f7e75 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # [Archipelago](https://archipelago.gg) ![Discord Shield](https://discordapp.com/api/guilds/731205301247803413/widget.png?style=shield) | [Install](https://github.com/ArchipelagoMW/Archipelago/releases) -Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, presently, Archipelago is also the randomizer itself. +Archipelago provides a generic framework for developing multiworld capability for game randomizers. In all cases, +presently, Archipelago is also the randomizer itself. Currently, the following games are supported: + * The Legend of Zelda: A Link to the Past * Factorio * Minecraft @@ -77,36 +79,57 @@ windows binaries. ## History -Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are: +Archipelago is built upon a strong legacy of brilliant hobbyists. We want to honor that legacy by showing it here. +The repositories which Archipelago is built upon, inspired by, or otherwise owes its gratitude to are: * [bonta0's MultiWorld](https://github.com/Bonta0/ALttPEntranceRandomizer/tree/multiworld_31) * [AmazingAmpharos' Entrance Randomizer](https://github.com/AmazingAmpharos/ALttPEntranceRandomizer) * [VT Web Randomizer](https://github.com/sporchia/alttp_vt_randomizer) * [Dessyreqt's alttprandomizer](https://github.com/Dessyreqt/alttprandomizer) -* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make the vast majority of Enemizer contributions. +* [Zarby89's](https://github.com/Ijwu/Enemizer/commits?author=Zarby89) + and [sosuke3's](https://github.com/Ijwu/Enemizer/commits?author=sosuke3) contributions to Enemizer, which make up the + vast majority of Enemizer contributions. -We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the path. Just because one person's name may be in a repository title does not mean that only one person made that project happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago but we hope to honor them fairly. +We recognize that there is a strong community of incredibly smart people that have come before us and helped pave the +path. Just because one person's name may be in a repository title does not mean that only one person made that project +happen. We can't hope to perfectly cover every single contribution that lead up to Archipelago, but we hope to honor +them fairly. ### Path to the Archipelago -Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. + +Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a +long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to +_MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as +"Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository +(as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. ## Running Archipelago -For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems. -If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md). +For most people, all you need to do is head over to +the [releases page](https://github.com/ArchipelagoMW/Archipelago/releases), then download and run the appropriate +installer, or AppImage for Linux-based systems. + +If you are a developer or are running on a platform with no compiled releases available, please see our doc on +[running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories -This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. + +This project makes use of multiple other projects. We wouldn't be here without these other repositories and the +contributions of their developers, past and present. * [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer) * [Enemizer](https://github.com/Ijwu/Enemizer) * [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer) ## Contributing -For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md) + +To contribute to Archipelago, including the WebHost, core program, or by adding a new game, see our +[Contributing guidelines](/docs/contributing.md). ## FAQ -For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/) + +For Frequently asked questions, please see the website's [FAQ Page](https://archipelago.gg/faq/en/). ## Code of Conduct -Please refer to our [code of conduct.](/docs/code_of_conduct.md) + +Please refer to our [code of conduct](/docs/code_of_conduct.md). diff --git a/UndertaleClient.py b/UndertaleClient.py index e1538ce81d..cdc21c561a 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): with open(os.path.join(ctx.save_game_folder, filename), "w") as f: toDraw = "" for i in range(20): - if i < len(str(ctx.item_names[l.item])): - toDraw += str(ctx.item_names[l.item])[i] + if i < len(str(ctx.item_names.lookup_in_slot(l.item))): + toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i] else: break f.write(toDraw) diff --git a/Utils.py b/Utils.py index 141b1dc7f8..eea81a2d32 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ class Version(typing.NamedTuple): return ".".join(str(item) for item in self) -__version__ = "0.4.6" +__version__ = "0.5.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -101,8 +101,7 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[ @functools.wraps(function) def wrap(self: S, arg: T) -> RetType: - cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]], - getattr(self, cache_name, None)) + cache: Optional[Dict[T, RetType]] = getattr(self, cache_name, None) if cache is None: res = function(self, arg) setattr(self, cache_name, {arg: res}) @@ -209,10 +208,11 @@ def output_path(*path: str) -> str: def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None: if is_windows: - os.startfile(filename) + os.startfile(filename) # type: ignore else: from shutil import which open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) + assert open_command, "Didn't find program for open_file! Please report this together with system details." subprocess.call([open_command, filename]) @@ -300,21 +300,21 @@ def get_options() -> Settings: return get_settings() -def persistent_store(category: str, key: typing.Any, value: typing.Any): +def persistent_store(category: str, key: str, value: typing.Any): path = user_path("_persistent_storage.yaml") - storage: dict = persistent_load() - category = storage.setdefault(category, {}) - category[key] = value + storage = persistent_load() + category_dict = storage.setdefault(category, {}) + category_dict[key] = value with open(path, "wt") as f: f.write(dump(storage, Dumper=Dumper)) -def persistent_load() -> typing.Dict[str, dict]: - storage = getattr(persistent_load, "storage", None) +def persistent_load() -> Dict[str, Dict[str, Any]]: + storage: Union[Dict[str, Dict[str, Any]], None] = getattr(persistent_load, "storage", None) if storage: return storage path = user_path("_persistent_storage.yaml") - storage: dict = {} + storage = {} if os.path.exists(path): try: with open(path, "r") as f: @@ -323,7 +323,7 @@ def persistent_load() -> typing.Dict[str, dict]: logging.debug(f"Could not read store: {e}") if storage is None: storage = {} - persistent_load.storage = storage + setattr(persistent_load, "storage", storage) return storage @@ -365,6 +365,7 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N except Exception as e: logging.debug(f"Could not store data package: {e}") + def get_default_adjuster_settings(game_name: str) -> Namespace: import LttPAdjuster adjuster_settings = Namespace() @@ -383,7 +384,9 @@ def get_adjuster_settings(game_name: str) -> Namespace: default_settings = get_default_adjuster_settings(game_name) # Fill in any arguments from the argparser that we haven't seen before - return Namespace(**vars(adjuster_settings), **{k:v for k,v in vars(default_settings).items() if k not in vars(adjuster_settings)}) + return Namespace(**vars(adjuster_settings), **{ + k: v for k, v in vars(default_settings).items() if k not in vars(adjuster_settings) + }) @cache_argsless @@ -407,13 +410,13 @@ safe_builtins = frozenset(( class RestrictedUnpickler(pickle.Unpickler): generic_properties_module: Optional[object] - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super(RestrictedUnpickler, self).__init__(*args, **kwargs) self.options_module = importlib.import_module("Options") self.net_utils_module = importlib.import_module("NetUtils") self.generic_properties_module = None - def find_class(self, module, name): + def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata @@ -437,7 +440,7 @@ class RestrictedUnpickler(pickle.Unpickler): raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") -def restricted_loads(s): +def restricted_loads(s: bytes) -> Any: """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() @@ -455,6 +458,9 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] + def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): + super().__init__(default_factory, **kwargs) + def __missing__(self, key): self[key] = value = self.default_factory(key) return value @@ -493,7 +499,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri file_handler.setFormatter(logging.Formatter(log_format)) class Filter(logging.Filter): - def __init__(self, filter_name, condition): + def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogRecord], bool]) -> None: super().__init__(filter_name) self.condition = condition @@ -544,7 +550,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri ) -def stream_input(stream, queue): +def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"): def queuer(): while 1: try: @@ -572,7 +578,7 @@ class VersionException(Exception): pass -def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str: +def chaining_prefix(index: int, labels: typing.Sequence[str]) -> str: text = "" max_label = len(labels) - 1 while index > max_label: @@ -595,7 +601,7 @@ def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}" -def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \ +def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit: typing.Optional[int] = None) \ -> typing.List[typing.Tuple[str, int]]: import jellyfish @@ -603,21 +609,55 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower()) / max(len(word1), len(word2))) - limit: int = limit if limit else len(wordlist) + limit = limit if limit else len(word_list) return list( map( lambda container: (container[0], int(container[1]*100)), # convert up to limit to int % sorted( - map(lambda candidate: - (candidate, get_fuzzy_ratio(input_word, candidate)), - wordlist), + map(lambda candidate: (candidate, get_fuzzy_ratio(input_word, candidate)), word_list), key=lambda element: element[1], - reverse=True)[0:limit] + reverse=True + )[0:limit] ) ) -def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]], suggest: str = "") \ +def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bool, str]: + picks = get_fuzzy_results(input_text, possible_answers, limit=2) + if len(picks) > 1: + dif = picks[0][1] - picks[1][1] + if picks[0][1] == 100: + return picks[0][0], True, "Perfect Match" + elif picks[0][1] < 75: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + elif dif > 5: + return picks[0][0], True, "Close Match" + else: + return picks[0][0], False, f"Too many close matches for '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + else: + if picks[0][1] > 90: + return picks[0][0], True, "Only Option Match" + else: + return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ + f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" + + +def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]: + if "did you mean " in text: + for question in ("Didn't find something that closely matches", + "Too many close matches"): + if text.startswith(question): + name = get_text_between(text, "did you mean '", + "'? (") + return f"!{command} {name}" + elif text.startswith("Missing: "): + return text.replace("Missing: ", "!hint_location ") + return None + + +def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") @@ -734,7 +774,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None: root.update() -def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): +def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))): """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" def sorter(element: Union[str, Dict[str, Any]]) -> str: if (not isinstance(element, str)): @@ -788,7 +828,7 @@ class DeprecateDict(dict): log_message: str should_error: bool - def __init__(self, message, error: bool = False) -> None: + def __init__(self, message: str, error: bool = False) -> None: self.log_message = message self.should_error = error super().__init__() diff --git a/WargrooveClient.py b/WargrooveClient.py index 77180502ce..c5fdeb3532 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -176,7 +176,7 @@ class WargrooveContext(CommonContext): if not os.path.isfile(path): open(path, 'w').close() # Announcing commander unlocks - item_name = self.item_names[network_item.item] + item_name = self.item_names.lookup_in_slot(network_item.item) if item_name in faction_table.keys(): for commander in faction_table[item_name]: logger.info(f"{commander.name} has been unlocked!") @@ -197,7 +197,7 @@ class WargrooveContext(CommonContext): open(print_path, 'w').close() with open(print_path, 'w') as f: f.write("Received " + - self.item_names[network_item.item] + + self.item_names.lookup_in_slot(network_item.item) + " from " + self.player_names[network_item.player]) f.close() @@ -342,7 +342,7 @@ class WargrooveContext(CommonContext): faction_items = 0 faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()] for network_item in self.items_received: - if self.item_names[network_item.item] in faction_item_names: + if self.item_names.lookup_in_slot(network_item.item) in faction_item_names: faction_items += 1 starting_groove = (faction_items - 1) * self.starting_groove_multiplier # Must be an integer larger than 0 diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cfdbe25ff2..22d1f19f6b 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -56,15 +56,6 @@ def get_datapackage(): return network_data_package -@api_endpoints.route('/datapackage_version') -@cache.cached() -def get_datapackage_versions(): - from worlds import AutoWorldRegister - - version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} - return version_package - - @api_endpoints.route('/datapackage_checksum') @cache.cached() def get_datapackage_checksums(): diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index bc9f74bace..3a86cb551d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -106,9 +106,9 @@ class WebHostContext(Context): static_gamespackage = self.gamespackage # this is shared across all rooms static_item_name_groups = self.item_name_groups static_location_name_groups = self.location_name_groups - self.gamespackage = {"Archipelago": static_gamespackage["Archipelago"]} # this may be modified by _load - self.item_name_groups = {} - self.location_name_groups = {} + self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load + self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})} + self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})} for game in list(multidata.get("datapackage", {})): game_data = multidata["datapackage"][game] @@ -270,15 +270,19 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, await ctx.shutdown_task except (KeyboardInterrupt, SystemExit): - pass - except Exception: + if ctx.saving: + ctx._save() + except Exception as e: with db_session: room = Room.get(id=room_id) room.last_port = -1 + logger.exception(e) raise + else: + if ctx.saving: + ctx._save() finally: try: - ctx._save() with (db_session): # ensure the Room does not spin up again on its own, minute of safety buffer room = Room.get(id=room_id) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a78560cb0b..a12dc0f4ae 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -6,7 +6,7 @@ import random import tempfile import zipfile from collections import Counter -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Set from flask import flash, redirect, render_template, request, session, url_for from pony.orm import commit, db_session @@ -16,6 +16,7 @@ from Generate import PlandoOptions, handle_name from Main import main as ERmain from Utils import __version__ from WebHostLib import app +from settings import ServerOptions, GeneratorOptions from worlds.alttp.EntranceRandomizer import parse_arguments from .check import get_yaml_data, roll_options from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID @@ -23,25 +24,22 @@ from .upload import upload_zip_to_db def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]: - plando_options = { - options_source.get("plando_bosses", ""), - options_source.get("plando_items", ""), - options_source.get("plando_connections", ""), - options_source.get("plando_texts", "") - } - plando_options -= {""} + plando_options: Set[str] = set() + for substr in ("bosses", "items", "connections", "texts"): + if options_source.get(f"plando_{substr}", substr in GeneratorOptions.plando_options): + plando_options.add(substr) server_options = { - "hint_cost": int(options_source.get("hint_cost", 10)), - "release_mode": options_source.get("release_mode", "goal"), - "remaining_mode": options_source.get("remaining_mode", "disabled"), - "collect_mode": options_source.get("collect_mode", "disabled"), - "item_cheat": bool(int(options_source.get("item_cheat", 1))), + "hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)), + "release_mode": options_source.get("release_mode", ServerOptions.release_mode), + "remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode), + "collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode), + "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), "server_password": options_source.get("server_password", None), } generator_options = { - "spoiler": int(options_source.get("spoiler", 0)), - "race": race + "spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)), + "race": race, } if race: diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 94f173df70..62ba86a566 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,6 +11,7 @@ import Options from Utils import local_path from worlds.AutoWorld import AutoWorldRegister from . import app, cache +from .generate import get_meta def create() -> None: @@ -27,26 +28,21 @@ def get_world_theme(game_name: str) -> str: def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]: - visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui world = AutoWorldRegister.world_types[world_name] if world.hidden or world.web.options_page is False: return redirect("games") + visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - # Exclude settings from options pages if their visibility is disabled - if visibility_flag in option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + start_collapsed = {"Game Options": False} + for group in world.web.option_groups: + start_collapsed[group.name] = group.start_collapsed return render_template( template, world_name=world_name, world=world, - option_groups=grouped_options, + option_groups=Options.get_option_groups(world, visibility_level=visibility_flag), + start_collapsed=start_collapsed, issubclass=issubclass, Options=Options, theme=get_world_theme(world_name), @@ -55,7 +51,7 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]: from .generate import start_generation - return start_generation(options, {"plando_options": ["items", "connections", "texts", "bosses"]}) + return start_generation(options, get_meta({})) def send_yaml(player_name: str, formatted_options: dict) -> Response: @@ -80,6 +76,34 @@ def test_ordered(obj): def option_presets(game: str) -> Response: world = AutoWorldRegister.world_types[game] + presets = {} + for preset_name, preset in world.web.options_presets.items(): + presets[preset_name] = {} + for preset_option_name, preset_option in preset.items(): + if preset_option == "random": + presets[preset_name][preset_option_name] = preset_option + continue + + option = world.options_dataclass.type_hints[preset_option_name].from_any(preset_option) + if isinstance(option, Options.NamedRange) and isinstance(preset_option, str): + assert preset_option in option.special_range_names, \ + f"Invalid preset value '{preset_option}' for '{preset_option_name}' in '{preset_name}'. " \ + f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." + + presets[preset_name][preset_option_name] = option.value + elif isinstance(option, Options.Range): + presets[preset_name][preset_option_name] = option.value + elif isinstance(preset_option, str): + # Ensure the option value is valid for Choice and Toggle options + assert option.name_lookup[option.value] == preset_option, \ + f"Invalid option value '{preset_option}' for '{preset_option_name}' in preset '{preset_name}'. " \ + f"Values must not be resolved to a different option via option.from_text (or an alias)." + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + else: + # Use the name of the option + presets[preset_name][preset_option_name] = option.current_key + class SetEncoder(json.JSONEncoder): def default(self, obj): from collections.abc import Set @@ -87,7 +111,7 @@ def option_presets(game: str) -> Response: return list(obj) return json.JSONEncoder.default(self, obj) - json_data = json.dumps(world.web.options_presets, cls=SetEncoder) + json_data = json.dumps(presets, cls=SetEncoder) response = Response(json_data) response.headers["Content-Type"] = "application/json" return response @@ -173,9 +197,9 @@ def generate_yaml(game: str): else: options[key] = val - # Detect and build ItemDict options from their name pattern for key, val in options.copy().items(): key_parts = key.rsplit("||", 2) + # Detect and build ItemDict options from their name pattern if key_parts[-1] == "qty": if key_parts[0] not in options: options[key_parts[0]] = {} @@ -183,6 +207,13 @@ def generate_yaml(game: str): options[key_parts[0]][key_parts[1]] = int(val) del options[key] + # Detect keys which end with -custom, indicating a TextChoice with a possible custom value + elif key_parts[-1].endswith("-custom"): + if val: + options[key_parts[-1][:-7]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index b8e089ece5..6324837b28 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -27,7 +27,7 @@ const adjustTableHeight = () => { * @returns {string} */ const secondsToHours = (seconds) => { - let hours = Math.floor(seconds / 3600); + let hours = Math.floor(seconds / 3600); let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); return `${hours}:${minutes}`; }; @@ -38,18 +38,18 @@ window.addEventListener('load', () => { info: false, dom: "t", stateSave: true, - stateSaveCallback: function(settings, data) { + stateSaveCallback: function (settings, data) { delete data.search; localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data)); }, - stateLoadCallback: function(settings) { + stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, - footerCallback: function(tfoot, data, start, end, display) { + footerCallback: function (tfoot, data, start, end, display) { if (tfoot) { const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = - (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; } }, columnDefs: [ @@ -123,49 +123,64 @@ window.addEventListener('load', () => { event.preventDefault(); } }); - const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); - const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; + const target_second = parseInt(document.getElementById('tracker-wrapper').getAttribute('data-second')) + 3; + console.log("Target second of refresh: " + target_second); - function getSleepTimeSeconds(){ + function getSleepTimeSeconds() { // -40 % 60 is -40, which is absolutely wrong and should burn var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60; return sleepSeconds || 60; } + let update_on_view = false; const update = () => { - const target = $("
"); - console.log("Updating Tracker..."); - target.load(location.href, function (response, status) { - if (status === "success") { - target.find(".table").each(function (i, new_table) { - const new_trs = $(new_table).find("tbody>tr"); - const footer_tr = $(new_table).find("tfoot>tr"); - const old_table = tables.eq(i); - const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); - const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); - old_table.clear(); - if (footer_tr.length) { - $(old_table.table).find("tfoot").html(footer_tr); - } - old_table.rows.add(new_trs); - old_table.draw(); - $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); - $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); - }); - $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); - } else { - console.log("Failed to connect to Server, in order to update Table Data."); - console.log(response); - } - }) - setTimeout(update, getSleepTimeSeconds()*1000); + if (document.hidden) { + console.log("Document reporting as not visible, not updating Tracker..."); + update_on_view = true; + } else { + update_on_view = false; + const target = $("
"); + console.log("Updating Tracker..."); + target.load(location.href, function (response, status) { + if (status === "success") { + target.find(".table").each(function (i, new_table) { + const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); + const old_table = tables.eq(i); + const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); + const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); + old_table.clear(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); + $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); + $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); + }); + $("#multi-stream-link").replaceWith(target.find("#multi-stream-link")); + } else { + console.log("Failed to connect to Server, in order to update Table Data."); + console.log(response); + } + }) + } + updater = setTimeout(update, getSleepTimeSeconds() * 1000); } - setTimeout(update, getSleepTimeSeconds()*1000); + let updater = setTimeout(update, getSleepTimeSeconds() * 1000); window.addEventListener('resize', () => { adjustTableHeight(); tables.draw(); }); + window.addEventListener('visibilitychange', () => { + if (!document.hidden && update_on_view) { + console.log("Page became visible, tracker should be refreshed."); + clearTimeout(updater); + update(); + } + }); + adjustTableHeight(); }); diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2981c41452..2bbfe4ad01 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -24,7 +24,8 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker + and a Sphere Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/multispheretracker.html b/WebHostLib/templates/multispheretracker.html new file mode 100644 index 0000000000..a866974983 --- /dev/null +++ b/WebHostLib/templates/multispheretracker.html @@ -0,0 +1,72 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Sphere Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + +
+
+ + +
+ {% if tracker_data.get_spheres() %} + This tracker lists already found locations by their logical access sphere. + It ignores items that cannot be sent + and will therefore differ from the sphere numbers in the spoiler playthrough. + This tracker will automatically update itself periodically. + {% else %} + This Multiworld has no Sphere data, likely due to being too old, cannot display data. + {% endif %} +
+
+ +
+ {%- for team, players in tracker_data.get_all_players().items() %} +
+ + + + + {#- Mimicking hint table header for familiarity. #} + + + + + + + + + {%- for sphere in tracker_data.get_spheres() %} + {%- set current_sphere = loop.index %} + {%- for player, sphere_location_ids in sphere.items() %} + {%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %} + {%- set finder_game = tracker_data.get_player_game(team, player) %} + {%- set player_location_data = tracker_data.get_player_locations(team, player) %} + {%- for location_id in sphere_location_ids.intersection(checked_locations) %} + + {%- set item_id, receiver, item_flags = player_location_data[location_id] %} + {%- set receiver_game = tracker_data.get_player_game(team, receiver) %} + + + + + + + + {%- endfor %} + + {%- endfor %} + {%- endfor %} + +
SphereFinderReceiverItemLocationGame
{{ current_sphere }}{{ tracker_data.get_player_name(team, player) }}{{ tracker_data.get_player_name(team, receiver) }}{{ tracker_data.item_id_to_name[receiver_game][item_id] }}{{ tracker_data.location_id_to_name[finder_game][location_id] }}{{ finder_game }}
+
+ + {%- endfor -%} +
+
+{% endblock %} diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html index b16d4714ec..1b371b1229 100644 --- a/WebHostLib/templates/multitracker.html +++ b/WebHostLib/templates/multitracker.html @@ -10,7 +10,7 @@ {% include "header/dirtHeader.html" %} {% include "multitrackerNavigation.html" %} -
+
diff --git a/WebHostLib/templates/ootTracker.html b/WebHostLib/templates/ootTracker.html deleted file mode 100644 index ea7a6d5a4c..0000000000 --- a/WebHostLib/templates/ootTracker.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - {{ player_name }}'s Tracker - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
{{ hookshot_length }}
-
-
-
- -
{{ bottle_count if bottle_count > 0 else '' }}
-
-
-
- -
{{ wallet_size }}
-
-
-
- -
Zelda
-
-
-
- -
Epona
-
-
-
- -
Saria
-
-
-
- -
Sun
-
-
-
- -
Time
-
-
-
- -
Storms
-
-
-
- -
{{ token_count }}
-
-
-
- -
Min
-
-
-
- -
Bol
-
-
-
- -
Ser
-
-
-
- -
Req
-
-
-
- -
Noc
-
-
-
- -
Pre
-
-
-
- -
{{ piece_count if piece_count > 0 else '' }}
-
-
- - - - - - - - {% for area in checks_done %} - - - - - - - - {% for location in location_info[area] %} - - - - - - - {% endfor %} - - {% endfor %} -
Items
{{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
{{ location }}{{ '✔' if location_info[area][location] else '' }}
-
- - diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index c4d97255d8..b34ac79a02 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -141,7 +141,7 @@ {% for group_name in world.location_name_groups.keys()|sort %} {% if group_name != "Everywhere" %}
- +
{% endif %} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index 5657610914..2506cf9619 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -69,7 +69,7 @@
{% for group_name, group_options in option_groups.items() %} -
+
{{ group_name }}
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 91474d7696..5b8944a438 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -142,7 +142,7 @@ {% for group_name in world.location_name_groups.keys()|sort %} {% if group_name != "Everywhere" %}
- +
{% endif %} diff --git a/WebHostLib/templates/weightedOptions/weightedOptions.html b/WebHostLib/templates/weightedOptions/weightedOptions.html index c21671a863..b3aefd4835 100644 --- a/WebHostLib/templates/weightedOptions/weightedOptions.html +++ b/WebHostLib/templates/weightedOptions/weightedOptions.html @@ -51,7 +51,7 @@
{% for group_name, group_options in option_groups.items() %} -
+
{{ group_name }} {% for option_name, option in group_options.items() %}
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fd233da131..36ebaacbcb 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -3,8 +3,9 @@ import collections from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple, Counter from uuid import UUID +from email.utils import parsedate_to_datetime -from flask import render_template +from flask import render_template, make_response, Response, request from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -291,47 +292,47 @@ class TrackerData: return video_feeds + @_cache_results + def get_spheres(self) -> List[List[int]]: + """ each sphere is { player: { location_id, ... } } """ + return self._multidata.get("spheres", []) + + +def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: + if not room: + abort(404) + + if_modified = incoming_request.headers.get("If-Modified-Since", None) + if if_modified: + if_modified = parsedate_to_datetime(if_modified) + # if_modified has less precision than last_activity, so we bring them to same precision + if if_modified >= room.last_activity.replace(microsecond=0): + return make_response("", 304) + @app.route("/tracker///") -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" - tracker_page = cache.get(key) - if tracker_page: - return tracker_page + response: Optional[Response] = cache.get(key) + if response: + return response - timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) - cache.set(key, tracker_page, timeout) - return tracker_page - - -@app.route("/generic_tracker///") -def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: - return get_player_tracker(tracker, tracked_team, tracked_player, True) - - -@app.route("/tracker/", defaults={"game": "Generic"}) -@app.route("/tracker//") -@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) -def get_multiworld_tracker(tracker: UUID, game: str): # Room must exist. room = Room.get(tracker=tracker) - if not room: - abort(404) - tracker_data = TrackerData(room) - enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) - if game not in _multiworld_trackers: - return render_generic_multiworld_tracker(tracker_data, enabled_trackers) + response = _process_if_request_valid(request, room) + if response: + return response - return _multiworld_trackers[game](tracker_data, enabled_trackers) + timeout, last_modified, tracker_page = get_timeout_and_player_tracker(room, tracked_team, tracked_player, generic) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response -def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: - # Room must exist. - room = Room.get(tracker=tracker) - if not room: - abort(404) - +def get_timeout_and_player_tracker(room: Room, tracked_team: int, tracked_player: int, generic: bool)\ + -> Tuple[int, datetime.datetime, str]: tracker_data = TrackerData(room) # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. @@ -341,7 +342,48 @@ def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: in else: tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) + + +@app.route("/generic_tracker///") +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> Response: + return get_player_tracker(tracker, tracked_team, tracked_player, True) + + +@app.route("/tracker/", defaults={"game": "Generic"}) +@app.route("/tracker//") +def get_multiworld_tracker(tracker: UUID, game: str) -> Response: + key = f"{tracker}_{game}" + response: Optional[Response] = cache.get(key) + if response: + return response + + # Room must exist. + room = Room.get(tracker=tracker) + + response = _process_if_request_valid(request, room) + if response: + return response + + timeout, last_modified, tracker_page = get_timeout_and_multiworld_tracker(room, game) + response = make_response(tracker_page) + response.last_modified = last_modified + cache.set(key, response, timeout) + return response + + +def get_timeout_and_multiworld_tracker(room: Room, game: str)\ + -> Tuple[int, datetime.datetime, str]: + tracker_data = TrackerData(room) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game in _multiworld_trackers: + tracker = _multiworld_trackers[game](tracker_data, enabled_trackers) + else: + tracker = render_generic_multiworld_tracker(tracker_data, enabled_trackers) + + return ((tracker_data.get_room_saving_second() - datetime.datetime.now().second) + % TRACKER_CACHE_TIMEOUT_IN_SECONDS or TRACKER_CACHE_TIMEOUT_IN_SECONDS, room.last_activity, tracker) def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: @@ -411,9 +453,30 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker videos=tracker_data.get_room_videos(), item_id_to_name=tracker_data.item_id_to_name, location_id_to_name=tracker_data.location_id_to_name, + saving_second=tracker_data.get_room_saving_second(), ) +def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str: + return render_template( + "multispheretracker.html", + room=tracker_data.room, + tracker_data=tracker_data, + ) + + +@app.route("/sphere_tracker/") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_sphere_tracker(tracker: UUID): + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) + + tracker_data = TrackerData(room) + return render_generic_multiworld_sphere_tracker(tracker_data) + + # TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to # live in their respective world folders. diff --git a/Zelda1Client.py b/Zelda1Client.py index cd76a0a5ca..6d7af0a94d 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = ctx.location_names[location] + location_name = ctx.location_names.lookup_in_slot(location) if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index c34046d5dc..f54132e24a 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -6,10 +6,6 @@ # # All usernames must be GitHub usernames (and are case sensitive). -################### -## Active Worlds ## -################### - # Adventure /worlds/adventure/ @JusticePS @@ -67,9 +63,6 @@ # Factorio /worlds/factorio/ @Berserker66 -# Final Fantasy -/worlds/ff1/ @jtoyoda - # Final Fantasy Mystic Quest /worlds/ffmq/ @Alchav @wildham0 @@ -207,7 +200,7 @@ /worlds/yoshisisland/ @PinkSwitch #Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 -/worlds/yugioh06/ @rensen +/worlds/yugioh06/ @Rensen3 # Zillion /worlds/zillion/ @beauxq @@ -215,9 +208,22 @@ # Zork Grand Inquisitor /worlds/zork_grand_inquisitor/ @nbrochu -################################## -## Disabled Unmaintained Worlds ## -################################## + +## Active Unmaintained Worlds + +# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks +# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for +# any of these worlds, please review `/docs/world maintainer.md` documentation. + +# Final Fantasy (1) +# /worlds/ff1/ + + +## Disabled Unmaintained Worlds + +# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are +# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md` +# documentation. # Ori and the Blind Forest -# /worlds_disabled/oribf/ +# /worlds_disabled/oribf/ diff --git a/docs/contributing.md b/docs/contributing.md index e7f3516712..9fd21408eb 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,43 +1,49 @@ # Contributing -Contributions are welcome. We have a few requests for new contributors: + +All contributions are welcome, though we have a few requests of contributors, whether they be for core, webhost, or new +game contributions: * **Follow styling guidelines.** Please take a look at the [code style documentation](/docs/style.md) to ensure ease of communication and uniformity. -* **Ensure that critical changes are covered by tests.** -It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working. -If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md). -If you wish to contribute to the website, please take a look at [these tests](/test/webhost). +* **Ensure that critical changes are covered by tests.** + It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working. + If you wish to contribute by adding a new game, please take a look at + the [logic unit test documentation](/docs/tests.md). + If you wish to contribute to the website, please take a look at [these tests](/test/webhost). * **Do not introduce unit test failures/regressions.** -Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test -your changes. Currently, the oldest supported version is [Python 3.8](https://www.python.org/downloads/release/python-380/). -It is recommended that automated github actions are turned on in your fork to have github run all of the unit tests after pushing. -You can turn them on here: -![Github actions example](./img/github-actions-example.png) + Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test + your changes. Currently, the oldest supported version + is [Python 3.8](https://www.python.org/downloads/release/python-380/). + It is recommended that automated github actions are turned on in your fork to have github run unit tests after + pushing. + You can turn them on here: + ![Github actions example](./img/github-actions-example.png) * **When reviewing PRs, please leave a message about what was done.** -We don't have full test coverage, so manual testing can help. -For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing -or checking if all code paths are covered by automated tests is desired. The original author may not have been able -to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to -state which games or settings were rolled, if any. -Please also tell us if you looked at code, just did functional testing, did both, or did neither. -If testing the PR depends on other PRs, please state what you merged into what for testing. -We cannot determine what "LGTM" means without additional context, so that should not be the norm. + We don't have full test coverage, so manual testing can help. + For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing + or checking if all code paths are covered by automated tests is desired. The original author may not have been able + to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to + state which games or settings were rolled, if any. + Please also tell us if you looked at code, just did functional testing, did both, or did neither. + If testing the PR depends on other PRs, please state what you merged into what for testing. + We cannot determine what "LGTM" means without additional context, so that should not be the norm. -Other than these requests, we tend to judge code on a case-by-case basis. +Other than these requests, we tend to judge code on a case-by-case basis. For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md). If you want to contribute to the core, you will be subject to stricter review on your pull requests. It is recommended that you get in touch with other core maintainers via the [Discord](https://archipelago.gg/discord). -If you want to add Archipelago support for a new game, please take a look at the [adding games documentation](/docs/adding%20games.md), which details what is required -to implement support for a game, as well as tips for how to get started. -If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a -[world maintainer](/docs/world%20maintainer.md). +If you want to add Archipelago support for a new game, please take a look at +the [adding games documentation](/docs/adding%20games.md) +which details what is required to implement support for a game, and has tips on to get started. +If you want to merge a new game into the main Archipelago repo, please make sure to read the responsibilities as a +[world maintainer](/docs/world%20maintainer.md). -For other questions, feel free to explore the [main documentation folder](/docs/) and ask us questions in the #archipelago-dev channel -of the [Discord](https://archipelago.gg/discord). +For other questions, feel free to explore the [main documentation folder](/docs), and ask us questions in the +#ap-world-dev channel of the [Discord](https://archipelago.gg/discord). diff --git a/docs/network protocol.md b/docs/network protocol.md index 604ff6708f..da5c414315 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -53,7 +53,7 @@ Example: ``` ## (Server -> Client) -These packets are are sent from the multiworld server to the client. They are not messages which the server accepts. +These packets are sent from the multiworld server to the client. They are not messages which the server accepts. * [RoomInfo](#RoomInfo) * [ConnectionRefused](#ConnectionRefused) * [Connected](#Connected) @@ -80,7 +80,6 @@ Sent to clients when they connect to an Archipelago server. | hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | -| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | | datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. | | seed_name | str | Uniquely identifying name of this generation | | time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. | @@ -500,9 +499,9 @@ In JSON this may look like: {"item": 3, "location": 3, "player": 3, "flags": 0} ] ``` -`item` is the item id of the item. Item ids are in the range of ± 253-1. +`item` is the item id of the item. Item ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. -`location` is the location id of the item inside the world. Location ids are in the range of ± 253-1. +`location` is the location id of the item inside the world. Location ids are only supported in the range of [-253, 253 - 1], with anything ≤ 0 reserved for Archipelago use. `player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item @@ -646,15 +645,47 @@ class Hint(typing.NamedTuple): ``` ### Data Package Contents -A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings. +A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago +server most easily and not maintain their own mappings. Some contents include: -We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different version. A special case is datapackage version 0, where it is expected the package is custom and should not be cached. + - Name to ID mappings for items and locations. + - A checksum of each game's data package for clients to tell if a cached package is invalid. -Note: - * Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once. - * Any Name is unique to its type across its own Game only: Single Arrow can exist in two games. - * The IDs from the game "Archipelago" may be used in any other game. - Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory) +We encourage clients to cache the data package they receive on disk, or otherwise not tied to a session. You will know +when your cache is outdated if the [RoomInfo](#RoomInfo) packet or the datapackage itself denote a different checksum +than any locally cached ones. + +**Important Notes about IDs and Names**: + +* IDs ≤ 0 are reserved for "Archipelago" and should not be used by other world implementations. +* The IDs from the game "Archipelago" (in `worlds/generic`) may be used in any world. + * Especially Location ID `-1`: `Cheat Console` and `-2`: `Server` (typically Remote Start Inventory) +* Any names and IDs are only unique in its own world data package, but different games may reuse these names or IDs. + * At runtime, you will need to look up the game of the player to know which item or location ID/Name to lookup in the + data package. This can be easily achieved by reviewing the `slot_info` for a particular player ID prior to lookup. + * For example, a data package like this is valid (Some properties such as `checksum` were omitted): + ```json + { + "games": { + "Game A": { + "location_name_to_id": { + "Boss Chest": 40 + }, + "item_name_to_id": { + "Item X": 12 + } + }, + "Game B": { + "location_name_to_id": { + "Minigame Prize": 40 + }, + "item_name_to_id": { + "Item X": 40 + } + } + } + } + ``` #### Contents | Name | Type | Notes | @@ -668,7 +699,6 @@ GameData is a **dict** but contains these keys and values. It's broken out into |---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------| | item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. | | location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. | -| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. | | checksum | str | A checksum hash of this game's data. | ### Tags diff --git a/docs/world api.md b/docs/world api.md index 6714fa3a21..37638c3c66 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -121,6 +121,53 @@ class RLWeb(WebWorld): # ... ``` +* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations +or location groups. + + ```python + # locations.py + location_descriptions = { + "Red Potion #6": "In a secret destructible block under the second stairway", + "L2 Spaceship": """ + The group of all items in the spaceship in Level 2. + + This doesn't include the item on the spaceship door, since it can be + accessed without the Spaceship Key. + """ + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .locations import location_descriptions + + + class MyGameWeb(WebWorld): + location_descriptions = location_descriptions + ``` + +* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item +groups. + + ```python + # items.py + item_descriptions = { + "Red Potion": "A standard health potion", + "Spaceship Key": """ + The key to the spaceship in Level 2. + + This is necessary to get to the Star Realm. + """, + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .items import item_descriptions + + + class MyGameWeb(WebWorld): + item_descriptions = item_descriptions + ``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible @@ -178,36 +225,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being required, and will prevent progression and useful items from being placed at excluded locations. -#### Documenting Locations - -Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and -location groups. These descriptions will show up in location-selection options on the options pages. - -```python -# locations.py - -location_descriptions = { - "Red Potion #6": "In a secret destructible block under the second stairway", - "L2 Spaceship": - """ - The group of all items in the spaceship in Level 2. - - This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .locations import location_descriptions - - -class MyGameWorld(World): - location_descriptions = location_descriptions -``` - ### Items Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally @@ -232,36 +249,6 @@ Other classifications include: * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres -#### Documenting Items - -Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item -groups. These descriptions will show up in item-selection options on the options pages. - -```python -# items.py - -item_descriptions = { - "Red Potion": "A standard health potion", - "Spaceship Key": - """ - The key to the spaceship in Level 2. - - This is necessary to get to the Star Realm. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .items import item_descriptions - - -class MyGameWorld(World): - item_descriptions = item_descriptions -``` - ### Events An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to diff --git a/inno_setup.iss b/inno_setup.iss index 4b37279e8d..b016f224df 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -75,7 +75,7 @@ Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLaunc [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." -Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Flags: nowait; Components: lttp_sprites +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: lttp_sprites Filename: "{app}\ArchipelagoLauncher"; Parameters: "--update_settings"; StatusMsg: "Updating host.yaml..."; Flags: runasoriginaluser runhidden Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringChange('Launcher', '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/kvui.py b/kvui.py index a1663126cc..e9e495aef3 100644 --- a/kvui.py +++ b/kvui.py @@ -64,7 +64,7 @@ from kivy.uix.popup import Popup fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType -from Utils import async_start +from Utils import async_start, get_input_text_from_response if typing.TYPE_CHECKING: import CommonClient @@ -285,16 +285,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel): temp = MarkupLabel(text=self.text).markup text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) cmdinput = App.get_running_app().textinput - if not cmdinput.text and " did you mean " in text: - for question in ("Didn't find something that closely matches, did you mean ", - "Too many close matches, did you mean "): - if text.startswith(question): - name = Utils.get_text_between(text, question, - "? (") - cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}" - break - elif not cmdinput.text and text.startswith("Missing: "): - cmdinput.text = text.replace("Missing: ", "!hint_location ") + if not cmdinput.text: + input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command) + if input_text is not None: + cmdinput.text = input_text Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) return self.parent.select_with_touch(self.index, touch) @@ -683,10 +677,18 @@ class HintLog(RecycleView): for hint in hints: data.append({ "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, - "item": {"text": self.parser.handle_node( - {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "item": {"text": self.parser.handle_node({ + "type": "item_id", + "text": hint["item"], + "flags": hint["item_flags"], + "player": hint["receiving_player"], + })}, "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, - "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "location": {"text": self.parser.handle_node({ + "type": "location_id", + "text": hint["location"], + "player": hint["finding_player"], + })}, "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", "color": "blue", "text": hint["entrance"] if hint["entrance"] else "Vanilla"})}, diff --git a/settings.py b/settings.py index b463c5a047..7ab618c344 100644 --- a/settings.py +++ b/settings.py @@ -643,17 +643,6 @@ class GeneratorOptions(Group): PLAYTHROUGH = 2 FULL = 3 - class GlitchTriforceRoom(IntEnum): - """ - Glitch to Triforce room from Ganon - When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality - + hammer) and have completed the goal required for killing ganon to be able to access the triforce room. - 1 -> Enabled. - 0 -> Disabled (except in no-logic) - """ - OFF = 0 - ON = 1 - class PlandoOptions(str): """ List of options that can be plando'd. Can be combined, for example "bosses, items" @@ -665,6 +654,14 @@ class GeneratorOptions(Group): OFF = 0 ON = 1 + class PanicMethod(str): + """ + What to do if the current item placements appear unsolvable. + raise -> Raise an exception and abort. + swap -> Attempt to fix it by swapping prior placements around. (Default) + start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations. + """ + enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows player_files_path: PlayerFilesPath = PlayerFilesPath("Players") players: Players = Players(0) @@ -673,6 +670,7 @@ class GeneratorOptions(Group): spoiler: Spoiler = Spoiler(3) race: Race = Race(0) plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") + panic_method: PanicMethod = PanicMethod("swap") class SNIOptions(Group): diff --git a/setup.py b/setup.py index 3e128eec7e..54d5118a2c 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ from pathlib import Path # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze>=7.0.0' + requirement = 'cx-Freeze==7.0.0' import pkg_resources try: pkg_resources.require(requirement) diff --git a/test/general/test_client_server_interaction.py b/test/general/test_client_server_interaction.py new file mode 100644 index 0000000000..17de915174 --- /dev/null +++ b/test/general/test_client_server_interaction.py @@ -0,0 +1,23 @@ +import unittest + +from Utils import get_intended_text, get_input_text_from_response + + +class TestClient(unittest.TestCase): + def test_autofill_hint_from_fuzzy_hint(self) -> None: + tests = ( + ("item", ["item1", "item2"]), # Multiple close matches + ("itm", ["item1", "item21"]), # No close match, multiple option + ("item", ["item1"]), # No close match, single option + ("item", ["\"item\" 'item' (item)"]), # Testing different special characters + ) + + for input_text, possible_answers in tests: + item_name, usable, response = get_intended_text(input_text, possible_answers) + self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed") + + hint_command = get_input_text_from_response(response, "hint") + self.assertIsNotNone(hint_command, + "The response to fuzzy hints is no longer recognized by the hint autofill") + self.assertEqual(hint_command, f"!hint {item_name}", + "The hint command autofilled by the response is not correct") diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 98c41b67b1..e4010af394 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -6,22 +6,6 @@ from . import setup_solo_multiworld class TestIDs(unittest.TestCase): - def test_unique_items(self): - """Tests that every game has a unique ID per item in the datapackage""" - known_item_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_item_ids) - known_item_ids |= set(world_type.item_id_to_name) - self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current) - - def test_unique_locations(self): - """Tests that every game has a unique ID per location in the datapackage""" - known_location_ids = set() - for gamename, world_type in AutoWorldRegister.world_types.items(): - current = len(known_location_ids) - known_location_ids |= set(world_type.location_id_to_name) - self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current) - def test_range_items(self): """There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision.""" for gamename, world_type in AutoWorldRegister.world_types.items(): diff --git a/test/general/test_items.py b/test/general/test_items.py index 7c0b7050c6..9cc91a1b00 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -64,15 +64,6 @@ class TestBase(unittest.TestCase): for item in multiworld.itempool: self.assertIn(item.name, world_type.item_name_to_id) - def test_item_descriptions_have_valid_names(self): - """Ensure all item descriptions match an item name or item group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.item_names.union(world_type.item_name_groups) - for name in world_type.item_descriptions: - with self.subTest("Name should be valid", game=game_name, item=name): - self.assertIn(name, valid_names, - "All item descriptions must match defined item names") - def test_itempool_not_modified(self): """Test that worlds don't modify the itempool after `create_items`""" gen_steps = ("generate_early", "create_regions", "create_items") diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 2ac059312c..4b95ebd22c 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -66,12 +66,3 @@ class TestBase(unittest.TestCase): for location in locations: self.assertIn(location, world_type.location_name_to_id) self.assertNotIn(group_name, world_type.location_name_to_id) - - def test_location_descriptions_have_valid_names(self): - """Ensure all location descriptions match a location name or location group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.location_names.union(world_type.location_name_groups) - for name in world_type.location_descriptions: - with self.subTest("Name should be valid", game=game_name, location=name): - self.assertIn(name, valid_names, - "All location descriptions must match defined location names") diff --git a/test/general/test_player_options.py b/test/general/test_player_options.py index 9650fbe97a..ea7f19e3d9 100644 --- a/test/general/test_player_options.py +++ b/test/general/test_player_options.py @@ -31,7 +31,7 @@ class TestPlayerOptions(unittest.TestCase): self.assertEqual(new_weights["list_2"], ["string_3"]) self.assertEqual(new_weights["list_1"], ["string", "string_2"]) self.assertEqual(new_weights["dict_1"]["option_a"], 50) - self.assertEqual(new_weights["dict_1"]["option_b"], 0) + self.assertEqual(new_weights["dict_1"]["option_b"], 50) self.assertEqual(new_weights["dict_1"]["option_c"], 50) self.assertNotIn("option_f", new_weights["dict_2"]) self.assertEqual(new_weights["dict_2"]["option_g"], 50) diff --git a/test/programs/test_common_client.py b/test/programs/test_common_client.py new file mode 100644 index 0000000000..9936240d17 --- /dev/null +++ b/test/programs/test_common_client.py @@ -0,0 +1,106 @@ +import unittest + +import NetUtils +from CommonClient import CommonContext + + +class TestCommonContext(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.ctx = CommonContext() + self.ctx.slot = 1 # Pretend we're player 1 for this. + self.ctx.slot_info.update({ + 1: NetUtils.NetworkSlot("Player 1", "__TestGame1", NetUtils.SlotType.player), + 2: NetUtils.NetworkSlot("Player 2", "__TestGame1", NetUtils.SlotType.player), + 3: NetUtils.NetworkSlot("Player 3", "__TestGame2", NetUtils.SlotType.player), + }) + self.ctx.consume_players_package([ + NetUtils.NetworkPlayer(1, 1, "Player 1", "Player 1"), + NetUtils.NetworkPlayer(1, 2, "Player 2", "Player 2"), + NetUtils.NetworkPlayer(1, 3, "Player 3", "Player 3"), + ]) + # Using IDs outside the "safe range" for testing purposes only. If this fails unit tests, it's because + # another world is not following the spec for allowed ID ranges. + self.ctx.update_data_package({ + "games": { + "__TestGame1": { + "location_name_to_id": { + "Test Location 1 - Safe": 2**54 + 1, + "Test Location 2 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 1 - Safe": 2**54 + 1, + "Test Item 2 - Duplicate": 2**54 + 2, + }, + }, + "__TestGame2": { + "location_name_to_id": { + "Test Location 3 - Duplicate": 2**54 + 2, + }, + "item_name_to_id": { + "Test Item 3 - Duplicate": 2**54 + 2, + }, + }, + }, + }) + + async def test_archipelago_datapackage_lookups_exist(self): + assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist" + assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist" + + async def test_implicit_name_lookups(self): + # Items + assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe" + assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names[-1] == "Nothing" + + # Locations + assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe" + assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names[-1] == "Cheat Console" + + async def test_explicit_name_lookups(self): + # Items + assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe" + assert self.ctx.item_names["__TestGame1"][2**54+2] == "Test Item 2 - Duplicate" + assert self.ctx.item_names["__TestGame1"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame1"][-1] == "Nothing" + assert self.ctx.item_names["__TestGame2"][2**54+1] == f"Unknown item (ID: {2**54+1})" + assert self.ctx.item_names["__TestGame2"][2**54+2] == "Test Item 3 - Duplicate" + assert self.ctx.item_names["__TestGame2"][2**54+3] == f"Unknown item (ID: {2**54+3})" + assert self.ctx.item_names["__TestGame2"][-1] == "Nothing" + + # Locations + assert self.ctx.location_names["__TestGame1"][2**54+1] == "Test Location 1 - Safe" + assert self.ctx.location_names["__TestGame1"][2**54+2] == "Test Location 2 - Duplicate" + assert self.ctx.location_names["__TestGame1"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame1"][-1] == "Cheat Console" + assert self.ctx.location_names["__TestGame2"][2**54+1] == f"Unknown location (ID: {2**54+1})" + assert self.ctx.location_names["__TestGame2"][2**54+2] == "Test Location 3 - Duplicate" + assert self.ctx.location_names["__TestGame2"][2**54+3] == f"Unknown location (ID: {2**54+3})" + assert self.ctx.location_names["__TestGame2"][-1] == "Cheat Console" + + async def test_lookup_helper_functions(self): + # Checking own slot. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 3) == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_slot(-1) == f"Nothing" + + # Checking others' slots. + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 2) == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 2) == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 1, 3) == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_slot(2 ** 54 + 2, 3) == "Test Item 3 - Duplicate" + + # Checking by game. + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame1") == "Test Item 1 - Safe" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame1") == "Test Item 2 - Duplicate" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 3, "__TestGame1") == f"Unknown item (ID: {2 ** 54 + 3})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 1, "__TestGame2") == f"Unknown item (ID: {2 ** 54 + 1})" + assert self.ctx.item_names.lookup_in_game(2 ** 54 + 2, "__TestGame2") == "Test Item 3 - Duplicate" + + # Checking with Archipelago ids are valid in any game package. + assert self.ctx.item_names.lookup_in_slot(-1, 2) == "Nothing" + assert self.ctx.item_names.lookup_in_slot(-1, 3) == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame1") == "Nothing" + assert self.ctx.item_names.lookup_in_game(-1, "__TestGame2") == "Nothing" diff --git a/test/webhost/test_descriptions.py b/test/webhost/test_descriptions.py new file mode 100644 index 0000000000..70f375b51c --- /dev/null +++ b/test/webhost/test_descriptions.py @@ -0,0 +1,23 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister + + +class TestWebDescriptions(unittest.TestCase): + def test_item_descriptions_have_valid_names(self) -> None: + """Ensure all item descriptions match an item name or item group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.item_names.union(world_type.item_name_groups) + for name in world_type.web.item_descriptions: + with self.subTest("Name should be valid", game=game_name, item=name): + self.assertIn(name, valid_names, + "All item descriptions must match defined item names") + + def test_location_descriptions_have_valid_names(self) -> None: + """Ensure all location descriptions match a location name or location group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.location_names.union(world_type.location_name_groups) + for name in world_type.web.location_descriptions: + with self.subTest("Name should be valid", game=game_name, location=name): + self.assertIn(name, valid_names, + "All location descriptions must match defined location names") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index b564932eb9..6e17f023f6 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,18 +3,14 @@ from __future__ import annotations import hashlib import logging import pathlib -from random import Random -import re import sys import time +from random import Random from dataclasses import make_dataclass -from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, - Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) +from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, + TYPE_CHECKING, Type, Union) -from Options import ( - ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, - PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints -) +from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -55,17 +51,12 @@ class AutoWorldRegister(type): dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] - dct["item_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("item_descriptions", {}).items()} - dct["item_descriptions"]["Everything"] = "All items in the entire game." + dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("location_name_groups", {}).items()} dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) - dct["location_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("location_descriptions", {}).items()} - dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." # move away from get_required_client_version function if "game" in dct: @@ -125,13 +116,19 @@ class WebWorldRegister(type): # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the # dev, putting it at the end if they don't define options in it option_groups: List[OptionGroup] = dct.get("option_groups", []) - item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, - StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] + prebuilt_options = ["Game Options", "Item & Location Options"] seen_options = [] item_group_in_list = False for group in option_groups: - assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined." + assert group.options, "A custom defined Option Group must contain at least one Option." + # catch incorrectly titled versions of the prebuilt groups so they don't create extra groups + title_name = group.name.title() + if title_name in prebuilt_options: + group.name = title_name + if group.name == "Item & Location Options": + assert not any(option in item_and_loc_options for option in group.options), \ + f"Item and Location Options cannot be specified multiple times" group.options.extend(item_and_loc_options) item_group_in_list = True else: @@ -143,7 +140,7 @@ class WebWorldRegister(type): assert option not in seen_options, f"{option} found in two option groups" seen_options.append(option) if not item_group_in_list: - option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options)) + option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options, True)) return super().__new__(mcs, name, bases, dct) @@ -226,6 +223,12 @@ class WebWorld(metaclass=WebWorldRegister): option_groups: ClassVar[List[OptionGroup]] = [] """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + location_descriptions: Dict[str, str] = {} + """An optional map from location names (or location group names) to brief descriptions for users.""" + + item_descriptions: Dict[str, str] = {} + """An optional map from item names (or item group names) to brief descriptions for users.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. @@ -252,35 +255,9 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" - item_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from item names (or item group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of items. - """ - location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - location_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from location names (or location group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of locations. - """ - - data_version: ClassVar[int] = 0 - """ - Increment this every time something in your world's names/id mappings changes. - - When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients - that it should not be cached, and clients should request that world's DataPackage every connection. Not - recommended for production-ready worlds. - - Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and - request a new DataPackage, if necessary. - """ - required_client_version: Tuple[int, int, int] = (0, 1, 6) """ override this if changes to a world break forward-compatibility of the client @@ -554,7 +531,6 @@ class World(metaclass=AutoWorldRegister): "item_name_to_id": cls.item_name_to_id, "location_name_groups": sorted_location_name_groups, "location_name_to_id": cls.location_name_to_id, - "version": cls.data_version, } res["checksum"] = data_package_checksum(res) return res @@ -572,18 +548,3 @@ def data_package_checksum(data: "GamesPackage") -> str: assert sorted(data) == list(data), "Data not ordered" from NetUtils import encode return hashlib.sha1(encode(data).encode()).hexdigest() - - -def _normalize_description(description): - """ - Normalizes a description in item_descriptions or location_descriptions. - - This allows authors to write descritions with nice indentation and line lengths in their world - definitions without having it affect the rendered format. - """ - # First, collapse the whitespace around newlines and the ends of the description. - description = re.sub(r' *\n *', '\n', description.strip()) - # Next, condense individual newlines into spaces. - description = re.sub(r'(? bool: return True except (TimeoutError, ConnectionRefusedError): continue - + # No ports worked ctx.streams = None ctx.connection_status = ConnectionStatus.NOT_CONNECTED diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 32a6e3704e..00370c277a 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -2,7 +2,6 @@ A module containing the BizHawkClient base class and metaclass """ - from __future__ import annotations import abc @@ -12,14 +11,13 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone if TYPE_CHECKING: from .context import BizHawkClientContext -else: - BizHawkClientContext = object def launch_client(*args) -> None: from .context import launch launch_subprocess(launch, name="BizHawkClient") + component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, file_identifier=SuffixIdentifier()) components.append(component) @@ -56,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta): return new_class @staticmethod - async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]: + async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]: for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): if system in systems: for handler in handlers.values(): @@ -77,7 +75,7 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): """The file extension(s) this client is meant to open and patch (e.g. ".apz3")""" @abc.abstractmethod - async def validate_rom(self, ctx: BizHawkClientContext) -> bool: + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: """Should return whether the currently loaded ROM should be handled by this client. You might read the game name from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the client class, so you do not need to check the system yourself. @@ -86,18 +84,18 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...).""" ... - async def set_auth(self, ctx: BizHawkClientContext) -> None: + async def set_auth(self, ctx: "BizHawkClientContext") -> None: """Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their username.""" pass @abc.abstractmethod - async def game_watcher(self, ctx: BizHawkClientContext) -> None: + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: """Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed to have passed your validator when this function is called, and the emulator is very likely to be connected.""" ... - def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: + def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" pass diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 05bee23412..9fe6c9e1ff 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -3,7 +3,6 @@ A module containing context and functions relevant to running the client. This m checking or launching the client, otherwise it will probably cause circular import issues. """ - import asyncio import enum import subprocess @@ -77,7 +76,7 @@ class BizHawkClientContext(CommonContext): if self.client_handler is not None: self.client_handler.on_package(self, cmd, args) - async def server_auth(self, password_requested: bool = False): + async def server_auth(self, password_requested: bool=False): self.password_requested = password_requested if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED: @@ -103,7 +102,7 @@ class BizHawkClientContext(CommonContext): await self.send_connect() self.auth_status = AuthStatus.PENDING - async def disconnect(self, allow_autoreconnect: bool = False): + async def disconnect(self, allow_autoreconnect: bool=False): self.auth_status = AuthStatus.NOT_AUTHENTICATED await super().disconnect(allow_autoreconnect) @@ -148,7 +147,8 @@ async def _game_watcher(ctx: BizHawkClientContext): script_version = await get_script_version(ctx.bizhawk_ctx) if script_version != EXPECTED_SCRIPT_VERSION: - logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.") + logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but " + f"got {script_version}. Disconnecting.") disconnect(ctx.bizhawk_ctx) continue @@ -177,7 +177,8 @@ async def _game_watcher(ctx: BizHawkClientContext): if ctx.client_handler is None: if not showed_no_handler_message: - logger.info("No handler was found for this game") + logger.info("No handler was found for this game. Double-check that the apworld is installed " + "correctly and that you loaded the right ROM file.") showed_no_handler_message = True continue else: diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 84caca828f..1c2583b3ed 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -113,7 +113,6 @@ class AdventureWorld(World): settings: ClassVar[AdventureSettings] item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} - data_version: ClassVar[int] = 1 required_client_version: Tuple[int, int, int] = (0, 3, 9) def __init__(self, world: MultiWorld, player: int): diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 6a388a98e8..0ba0f5b9a5 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -699,11 +699,14 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: # Needs to be at least moderate to cross the big dweller wall if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE: return False - elif act.name == "Your Contract has Expired" and diff < Difficulty.EXPERT: # Snatcher Hover - return False elif act.name == "Heating Up Mafia Town": # Straight up impossible return False + # Need to be able to hover + if act.name == "Your Contract has Expired": + if diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips: + return False + if act.name == "Dead Bird Studio": # No umbrella logic = moderate, umbrella logic = expert. if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT: @@ -718,14 +721,9 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: return False if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": - # This requires a cherry hover to enter Subcon - if act.name == "Your Contract has Expired": - if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: - return False - else: - # Only allow Subcon levels if paintings can be skipped - if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: - return False + # Only allow Subcon levels if painting skips are allowed + if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: + return False return True diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 5b27f559ef..db7555f246 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: def new_check(location_id): new_locations.append(location_id) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ' + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + @@ -552,9 +552,9 @@ class ALTTPSNIClient(SNIClient): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 8cb377b7a4..11c1a0165b 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,8 +1,11 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\ - FreeText, Removed +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ + StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed +from .EntranceShuffle import default_connections, default_dungeon_connections, \ + inverted_default_connections, inverted_default_dungeon_connections +from .Text import TextTable class GlitchesRequired(Choice): @@ -721,7 +724,27 @@ class AllowCollect(DefaultOnToggle): display_name = "Allow Collection of checks for other players" +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 ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + + +class ALttPPlandoTexts(PlandoTexts): + """Text plando. Format is: + - text: 'This is your text' + at: text_key + percentage: 100 + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + valid_keys = TextTable.valid_keys + + alttp_options: typing.Dict[str, type(Option)] = { + "plando_connections": ALttPPlandoConnections, + "plando_texts": ALttPPlandoTexts, "start_inventory_from_pool": StartInventoryPool, "goal": Goal, "mode": Mode, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 05113514e4..224de6aaf7 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1269,7 +1269,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) # set up goals for treasure hunt - rom.write_int16(0x180163, local_world.treasure_hunt_required) + rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1372,7 +1373,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', 'Red Mail', 'Blue Mail', 'Progressive Mail', - 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)'}: + 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}: continue set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), @@ -2475,6 +2476,9 @@ def write_strings(rom, world, player): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] + triforce_pieces_required = max(0, w.treasure_hunt_required - + sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")) + if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2482,16 +2486,16 @@ def write_strings(rom, world, player): tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' else: tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' @@ -2500,20 +2504,20 @@ def write_strings(rom, world, player): tt['ganon_fall_in'] = Ganon1_texts[local_random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' - if w.treasure_hunt_required > 1: + if triforce_pieces_required > 1: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) else: if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ - (w.treasure_hunt_required, w.treasure_hunt_total) + (triforce_pieces_required, w.treasure_hunt_total) tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)] @@ -2538,12 +2542,12 @@ def write_strings(rom, world, player): tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" - for at, text in world.plando_texts[player].items(): + for at, text, _ in world.plando_texts[player]: if at not in tt: raise Exception(f"No text target \"{at}\" found.") else: - tt[at] = text + tt[at] = "\n".join(text) rom.write_bytes(0xE0000, tt.getBytes()) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index dbe8cc1f9d..db2b5b680c 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -9,9 +9,9 @@ from worlds.generic.Rules import add_rule from BaseClasses import CollectionState from .SubClasses import ALttPLocation -from .EntranceShuffle import door_addresses + from .Items import item_name_groups -from .Options import small_key_shuffle, RandomizeShopInventories + from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows logger = logging.getLogger("Shops") @@ -66,6 +66,7 @@ class Shop: return 0 def get_bytes(self) -> List[int]: + from .EntranceShuffle import door_addresses # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] entrances = self.region.entrances config = self.item_count @@ -181,7 +182,7 @@ def push_shop_inventories(multiworld): def create_shops(multiworld, player: int): - + from .Options import RandomizeShopInventories player_shop_table = shop_table.copy() if multiworld.include_witch_hut[player]: player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) @@ -304,6 +305,7 @@ shop_generation_types = { def set_up_shops(multiworld, player: int): + from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops if multiworld.retro_bow[player]: @@ -426,7 +428,7 @@ def get_price_modifier(item): def get_price(multiworld, item, player: int, price_type=None): """Converts a raw Rupee price into a special price type""" - + from .Options import small_key_shuffle if price_type: price_types = [price_type] else: diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py index b479a9b8e0..c005cacd8f 100644 --- a/worlds/alttp/Text.py +++ b/worlds/alttp/Text.py @@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper): class TextTable(object): SIZE = 0x7355 + valid_keys = [ + "set_cursor", + "set_cursor2", + "game_over_menu", + "var_test", + "follower_no_enter", + "choice_1_3", + "choice_2_3", + "choice_3_3", + "choice_1_2", + "choice_2_2", + "uncle_leaving_text", + "uncle_dying_sewer", + "tutorial_guard_1", + "tutorial_guard_2", + "tutorial_guard_3", + "tutorial_guard_4", + "tutorial_guard_5", + "tutorial_guard_6", + "tutorial_guard_7", + "priest_sanctuary_before_leave", + "sanctuary_enter", + "zelda_sanctuary_story", + "priest_sanctuary_before_pendants", + "priest_sanctuary_after_pendants_before_master_sword", + "priest_sanctuary_dying", + "zelda_save_sewers", + "priest_info", + "zelda_sanctuary_before_leave", + "telepathic_intro", + "telepathic_reminder", + "zelda_go_to_throne", + "zelda_push_throne", + "zelda_switch_room_pull", + "zelda_save_lets_go", + "zelda_save_repeat", + "zelda_before_pendants", + "zelda_after_pendants_before_master_sword", + "telepathic_zelda_right_after_master_sword", + "zelda_sewers", + "zelda_switch_room", + "kakariko_saharalasa_wife", + "kakariko_saharalasa_wife_sword_story", + "kakariko_saharalasa_wife_closing", + "kakariko_saharalasa_after_master_sword", + "kakariko_alert_guards", + "sahasrahla_quest_have_pendants", + "sahasrahla_quest_have_master_sword", + "sahasrahla_quest_information", + "sahasrahla_bring_courage", + "sahasrahla_have_ice_rod", + "telepathic_sahasrahla_beat_agahnim", + "telepathic_sahasrahla_beat_agahnim_no_pearl", + "sahasrahla_have_boots_no_icerod", + "sahasrahla_have_courage", + "sahasrahla_found", + "sign_rain_north_of_links_house", + "sign_north_of_links_house", + "sign_path_to_death_mountain", + "sign_lost_woods", + "sign_zoras", + "sign_outside_magic_shop", + "sign_death_mountain_cave_back", + "sign_east_of_links_house", + "sign_south_of_lumberjacks", + "sign_east_of_desert", + "sign_east_of_sanctuary", + "sign_east_of_castle", + "sign_north_of_lake", + "sign_desert_thief", + "sign_lumberjacks_house", + "sign_north_kakariko", + "witch_bring_mushroom", + "witch_brewing_the_item", + "witch_assistant_no_bottle", + "witch_assistant_no_empty_bottle", + "witch_assistant_informational", + "witch_assistant_no_bottle_buying", + "potion_shop_no_empty_bottles", + "item_get_lamp", + "item_get_boomerang", + "item_get_bow", + "item_get_shovel", + "item_get_magic_cape", + "item_get_powder", + "item_get_flippers", + "item_get_power_gloves", + "item_get_pendant_courage", + "item_get_pendant_power", + "item_get_pendant_wisdom", + "item_get_mushroom", + "item_get_book", + "item_get_moonpearl", + "item_get_compass", + "item_get_map", + "item_get_ice_rod", + "item_get_fire_rod", + "item_get_ether", + "item_get_bombos", + "item_get_quake", + "item_get_hammer", + "item_get_flute", + "item_get_cane_of_somaria", + "item_get_hookshot", + "item_get_bombs", + "item_get_bottle", + "item_get_big_key", + "item_get_titans_mitts", + "item_get_magic_mirror", + "item_get_fake_mastersword", + "post_item_get_mastersword", + "item_get_red_potion", + "item_get_green_potion", + "item_get_blue_potion", + "item_get_bug_net", + "item_get_blue_mail", + "item_get_red_mail", + "item_get_temperedsword", + "item_get_mirror_shield", + "item_get_cane_of_byrna", + "missing_big_key", + "missing_magic", + "item_get_pegasus_boots", + "talking_tree_info_start", + "talking_tree_info_1", + "talking_tree_info_2", + "talking_tree_info_3", + "talking_tree_info_4", + "talking_tree_other", + "item_get_pendant_power_alt", + "item_get_pendant_wisdom_alt", + "game_shooting_choice", + "game_shooting_yes", + "game_shooting_no", + "game_shooting_continue", + "pond_of_wishing", + "pond_item_select", + "pond_item_test", + "pond_will_upgrade", + "pond_item_test_no", + "pond_item_test_no_no", + "pond_item_boomerang", + "pond_item_shield", + "pond_item_silvers", + "pond_item_bottle_filled", + "pond_item_sword", + "pond_of_wishing_happiness", + "pond_of_wishing_choice", + "pond_of_wishing_bombs", + "pond_of_wishing_arrows", + "pond_of_wishing_full_upgrades", + "mountain_old_man_first", + "mountain_old_man_deadend", + "mountain_old_man_turn_right", + "mountain_old_man_lost_and_alone", + "mountain_old_man_drop_off", + "mountain_old_man_in_his_cave_pre_agahnim", + "mountain_old_man_in_his_cave", + "mountain_old_man_in_his_cave_post_agahnim", + "tavern_old_man_awake", + "tavern_old_man_unactivated_flute", + "tavern_old_man_know_tree_unactivated_flute", + "tavern_old_man_have_flute", + "chicken_hut_lady", + "running_man", + "game_race_sign", + "sign_bumper_cave", + "sign_catfish", + "sign_north_village_of_outcasts", + "sign_south_of_bumper_cave", + "sign_east_of_pyramid", + "sign_east_of_bomb_shop", + "sign_east_of_mire", + "sign_village_of_outcasts", + "sign_before_wishing_pond", + "sign_before_catfish_area", + "castle_wall_guard", + "gate_guard", + "telepathic_tile_eastern_palace", + "telepathic_tile_tower_of_hera_floor_4", + "hylian_text_1", + "mastersword_pedestal_translated", + "telepathic_tile_spectacle_rock", + "telepathic_tile_swamp_entrance", + "telepathic_tile_thieves_town_upstairs", + "telepathic_tile_misery_mire", + "hylian_text_2", + "desert_entry_translated", + "telepathic_tile_under_ganon", + "telepathic_tile_palace_of_darkness", + "telepathic_tile_desert_bonk_torch_room", + "telepathic_tile_castle_tower", + "telepathic_tile_ice_large_room", + "telepathic_tile_turtle_rock", + "telepathic_tile_ice_entrance", + "telepathic_tile_ice_stalfos_knights_room", + "telepathic_tile_tower_of_hera_entrance", + "houlihan_room", + "caught_a_bee", + "caught_a_fairy", + "no_empty_bottles", + "game_race_boy_time", + "game_race_girl", + "game_race_boy_success", + "game_race_boy_failure", + "game_race_boy_already_won", + "game_race_boy_sneaky", + "bottle_vendor_choice", + "bottle_vendor_get", + "bottle_vendor_no", + "bottle_vendor_already_collected", + "bottle_vendor_bee", + "bottle_vendor_fish", + "hobo_item_get_bottle", + "blacksmiths_what_you_want", + "blacksmiths_paywall", + "blacksmiths_extra_okay", + "blacksmiths_tempered_already", + "blacksmiths_temper_no", + "blacksmiths_bogart_sword", + "blacksmiths_get_sword", + "blacksmiths_shop_before_saving", + "blacksmiths_shop_saving", + "blacksmiths_collect_frog", + "blacksmiths_still_working", + "blacksmiths_saving_bows", + "blacksmiths_hammer_anvil", + "dark_flute_boy_storytime", + "dark_flute_boy_get_shovel", + "dark_flute_boy_no_get_shovel", + "dark_flute_boy_flute_not_found", + "dark_flute_boy_after_shovel_get", + "shop_fortune_teller_lw_hint_0", + "shop_fortune_teller_lw_hint_1", + "shop_fortune_teller_lw_hint_2", + "shop_fortune_teller_lw_hint_3", + "shop_fortune_teller_lw_hint_4", + "shop_fortune_teller_lw_hint_5", + "shop_fortune_teller_lw_hint_6", + "shop_fortune_teller_lw_hint_7", + "shop_fortune_teller_lw_no_rupees", + "shop_fortune_teller_lw", + "shop_fortune_teller_lw_post_hint", + "shop_fortune_teller_lw_no", + "shop_fortune_teller_lw_hint_8", + "shop_fortune_teller_lw_hint_9", + "shop_fortune_teller_lw_hint_10", + "shop_fortune_teller_lw_hint_11", + "shop_fortune_teller_lw_hint_12", + "shop_fortune_teller_lw_hint_13", + "shop_fortune_teller_lw_hint_14", + "shop_fortune_teller_lw_hint_15", + "dark_sanctuary", + "dark_sanctuary_hint_0", + "dark_sanctuary_no", + "dark_sanctuary_hint_1", + "dark_sanctuary_yes", + "dark_sanctuary_hint_2", + "sick_kid_no_bottle", + "sick_kid_trade", + "sick_kid_post_trade", + "desert_thief_sitting", + "desert_thief_following", + "desert_thief_question", + "desert_thief_question_yes", + "desert_thief_after_item_get", + "desert_thief_reassure", + "hylian_text_3", + "tablet_ether_book", + "tablet_bombos_book", + "magic_bat_wake", + "magic_bat_give_half_magic", + "intro_main", + "intro_throne_room", + "intro_zelda_cell", + "intro_agahnim", + "pickup_purple_chest", + "bomb_shop", + "bomb_shop_big_bomb", + "bomb_shop_big_bomb_buy", + "item_get_big_bomb", + "kiki_second_extortion", + "kiki_second_extortion_no", + "kiki_second_extortion_yes", + "kiki_first_extortion", + "kiki_first_extortion_yes", + "kiki_first_extortion_no", + "kiki_leaving_screen", + "blind_in_the_cell", + "blind_by_the_light", + "blind_not_that_way", + "aginah_l1sword_no_book", + "aginah_l1sword_with_pendants", + "aginah", + "aginah_need_better_sword", + "aginah_have_better_sword", + "catfish", + "catfish_after_item", + "lumberjack_right", + "lumberjack_left", + "lumberjack_left_post_agahnim", + "fighting_brothers_right", + "fighting_brothers_right_opened", + "fighting_brothers_left", + "maiden_crystal_1", + "maiden_crystal_2", + "maiden_crystal_3", + "maiden_crystal_4", + "maiden_crystal_5", + "maiden_crystal_6", + "maiden_crystal_7", + "maiden_ending", + "maiden_confirm_understood", + "barrier_breaking", + "maiden_crystal_7_again", + "agahnim_zelda_teleport", + "agahnim_magic_running_away", + "agahnim_hide_and_seek_found", + "agahnim_defeated", + "agahnim_final_meeting", + "zora_meeting", + "zora_tells_cost", + "zora_get_flippers", + "zora_no_cash", + "zora_no_buy_item", + "kakariko_saharalasa_grandson", + "kakariko_saharalasa_grandson_next", + "dark_palace_tree_dude", + "fairy_wishing_ponds", + "fairy_wishing_ponds_no", + "pond_of_wishing_no", + "pond_of_wishing_return_item", + "pond_of_wishing_throw", + "pond_pre_item_silvers", + "pond_of_wishing_great_luck", + "pond_of_wishing_good_luck", + "pond_of_wishing_meh_luck", + "pond_of_wishing_bad_luck", + "pond_of_wishing_fortune", + "item_get_14_heart", + "item_get_24_heart", + "item_get_34_heart", + "item_get_whole_heart", + "item_get_sanc_heart", + "fairy_fountain_refill", + "death_mountain_bullied_no_pearl", + "death_mountain_bullied_with_pearl", + "death_mountain_bully_no_pearl", + "death_mountain_bully_with_pearl", + "shop_darkworld_enter", + "game_chest_village_of_outcasts", + "game_chest_no_cash", + "game_chest_not_played", + "game_chest_played", + "game_chest_village_of_outcasts_play", + "shop_first_time", + "shop_already_have", + "shop_buy_shield", + "shop_buy_red_potion", + "shop_buy_arrows", + "shop_buy_bombs", + "shop_buy_bee", + "shop_buy_heart", + "shop_first_no_bottle_buy", + "shop_buy_no_space", + "ganon_fall_in", + "ganon_phase_3", + "lost_woods_thief", + "blinds_hut_dude", + "end_triforce", + "toppi_fallen", + "kakariko_tavern_fisherman", + "thief_money", + "thief_desert_rupee_cave", + "thief_ice_rupee_cave", + "telepathic_tile_south_east_darkworld_cave", + "cukeman", + "cukeman_2", + "potion_shop_no_cash", + "kakariko_powdered_chicken", + "game_chest_south_of_kakariko", + "game_chest_play_yes", + "game_chest_play_no", + "game_chest_lost_woods", + "kakariko_flophouse_man_no_flippers", + "kakariko_flophouse_man", + "menu_start_2", + "menu_start_3", + "menu_pause", + "game_digging_choice", + "game_digging_start", + "game_digging_no_cash", + "game_digging_end_time", + "game_digging_come_back_later", + "game_digging_no_follower", + "menu_start_4", + "ganon_fall_in_alt", + "ganon_phase_3_alt", + "sign_east_death_mountain_bridge", + "fish_money", + "sign_ganons_tower", + "sign_ganon", + "ganon_phase_3_no_bow", + "ganon_phase_3_no_silvers_alt", + "ganon_phase_3_no_silvers", + "ganon_phase_3_silvers", + "murahdahla", + ] + def __init__(self): self._text = OrderedDict() self.setDefaultText() diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index ae3dfe9e3b..3176f7a7fc 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -213,7 +213,6 @@ class ALTTPWorld(World): item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} location_name_to_id = lookup_name_to_id - data_version = 9 required_client_version = (0, 4, 1) web = ALTTPWeb() diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index 5494c87e8c..34557d95d0 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -77,41 +77,41 @@ class ItemData: item_table = { # name: ID, Nb, Item Type, Item Group "Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone - "Arnassi statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue - "Big seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed - "Glowing seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed - "Black pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl - "Baby blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster - "Crab armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume - "Baby dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo + "Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue + "Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed + "Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed + "Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl + "Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster + "Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume + "Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo "Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss - "Energy statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue - "Krotite armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple - "Golden starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star - "Golden gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear - "Jelly beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon - "Jelly costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume - "Jelly plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant - "Mithalas doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll - "Mithalan dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume - "Mithalas banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner - "Mithalas pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot - "Mutant costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume - "Baby nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus - "Baby piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha + "Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue + "Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple + "Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star + "Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear + "Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon + "Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume + "Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant + "Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll + "Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume + "Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner + "Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot + "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume + "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus + "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume - "Seed bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag + "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull - "Song plant spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed - "Stone head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head - "Sun key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key - "Girl costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume - "Odd container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest + "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed + "Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head + "Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key + "Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume + "Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest "Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head - "Turtle egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg - "Jelly egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed - "Urchin costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume - "Baby walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker + "Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg + "Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed + "Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume + "Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker "Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All "Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi "Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice @@ -206,9 +206,9 @@ item_table = { "Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_openwater03 "Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04 - "Transturtle Home water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea + "Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea "Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03 "Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss - "Transturtle Simon says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 - "Transturtle Arnassi ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse + "Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05 + "Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse } diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index e4f6f104cc..7360efde06 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -29,213 +29,213 @@ class AquariaLocation(Location): class AquariaLocations: locations_verse_cave_r = { - "Verse cave, bulb in the skeleton room": 698107, - "Verse cave, bulb in the path left of the skeleton room": 698108, - "Verse cave right area, Big Seed": 698175, + "Verse Cave, bulb in the skeleton room": 698107, + "Verse Cave, bulb in the path left of the skeleton room": 698108, + "Verse Cave right area, Big Seed": 698175, } locations_verse_cave_l = { - "Verse cave, the Naija hint about here shield ability": 698200, - "Verse cave left area, bulb in the center part": 698021, - "Verse cave left area, bulb in the right part": 698022, - "Verse cave left area, bulb under the rock at the end of the path": 698023, + "Verse Cave, the Naija hint about the shield ability": 698200, + "Verse Cave left area, bulb in the center part": 698021, + "Verse Cave left area, bulb in the right part": 698022, + "Verse Cave left area, bulb under the rock at the end of the path": 698023, } locations_home_water = { - "Home water, bulb below the grouper fish": 698058, - "Home water, bulb in the path below Nautilus Prime": 698059, - "Home water, bulb in the little room above the grouper fish": 698060, - "Home water, bulb in the end of the left path from the verse cave": 698061, - "Home water, bulb in the top left path": 698062, - "Home water, bulb in the bottom left room": 698063, - "Home water, bulb close to the Naija's home": 698064, - "Home water, bulb under the rock in the left path from the verse cave": 698065, + "Home Water, bulb below the grouper fish": 698058, + "Home Water, bulb in the path below Nautilus Prime": 698059, + "Home Water, bulb in the little room above the grouper fish": 698060, + "Home Water, bulb in the end of the left path from the Verse Cave": 698061, + "Home Water, bulb in the top left path": 698062, + "Home Water, bulb in the bottom left room": 698063, + "Home Water, bulb close to Naija's Home": 698064, + "Home Water, bulb under the rock in the left path from the Verse Cave": 698065, } locations_home_water_nautilus = { - "Home water, Nautilus Egg": 698194, + "Home Water, Nautilus Egg": 698194, } locations_home_water_transturtle = { - "Home water, Transturtle": 698213, + "Home Water, Transturtle": 698213, } locations_naija_home = { - "Naija's home, bulb after the energy door": 698119, - "Naija's home, bulb under the rock at the right of the main path": 698120, + "Naija's Home, bulb after the energy door": 698119, + "Naija's Home, bulb under the rock at the right of the main path": 698120, } locations_song_cave = { - "Song cave, Erulian spirit": 698206, - "Song cave, bulb in the top left part": 698071, - "Song cave, bulb in the big anemone room": 698072, - "Song cave, bulb in the path to the singing statues": 698073, - "Song cave, bulb under the rock in the path to the singing statues": 698074, - "Song cave, bulb under the rock close to the song door": 698075, - "Song cave, Verse egg": 698160, - "Song cave, Jelly beacon": 698178, - "Song cave, Anemone seed": 698162, + "Song Cave, Erulian spirit": 698206, + "Song Cave, bulb in the top left part": 698071, + "Song Cave, bulb in the big anemone room": 698072, + "Song Cave, bulb in the path to the singing statues": 698073, + "Song Cave, bulb under the rock in the path to the singing statues": 698074, + "Song Cave, bulb under the rock close to the song door": 698075, + "Song Cave, Verse Egg": 698160, + "Song Cave, Jelly Beacon": 698178, + "Song Cave, Anemone Seed": 698162, } locations_energy_temple_1 = { - "Energy temple first area, beating the energy statue": 698205, - "Energy temple first area, bulb in the bottom room blocked by a rock": 698027, + "Energy Temple first area, beating the Energy Statue": 698205, + "Energy Temple first area, bulb in the bottom room blocked by a rock": 698027, } locations_energy_temple_idol = { - "Energy temple first area, Energy Idol": 698170, + "Energy Temple first area, Energy Idol": 698170, } locations_energy_temple_2 = { - "Energy temple second area, bulb under the rock": 698028, + "Energy Temple second area, bulb under the rock": 698028, } locations_energy_temple_altar = { - "Energy temple bottom entrance, Krotite armor": 698163, + "Energy Temple bottom entrance, Krotite Armor": 698163, } locations_energy_temple_3 = { - "Energy temple third area, bulb in the bottom path": 698029, + "Energy Temple third area, bulb in the bottom path": 698029, } locations_energy_temple_boss = { - "Energy temple boss area, Fallen god tooth": 698169, + "Energy Temple boss area, Fallen God Tooth": 698169, } locations_energy_temple_blaster_room = { - "Energy temple blaster room, Blaster egg": 698195, + "Energy Temple blaster room, Blaster Egg": 698195, } locations_openwater_tl = { - "Open water top left area, bulb under the rock in the right path": 698001, - "Open water top left area, bulb under the rock in the left path": 698002, - "Open water top left area, bulb to the right of the save cristal": 698003, + "Open Water top left area, bulb under the rock in the right path": 698001, + "Open Water top left area, bulb under the rock in the left path": 698002, + "Open Water top left area, bulb to the right of the save crystal": 698003, } locations_openwater_tr = { - "Open water top right area, bulb in the small path before Mithalas": 698004, - "Open water top right area, bulb in the path from the left entrance": 698005, - "Open water top right area, bulb in the clearing close to the bottom exit": 698006, - "Open water top right area, bulb in the big clearing close to the save cristal": 698007, - "Open water top right area, bulb in the big clearing to the top exit": 698008, - "Open water top right area, first urn in the Mithalas exit": 698148, - "Open water top right area, second urn in the Mithalas exit": 698149, - "Open water top right area, third urn in the Mithalas exit": 698150, + "Open Water top right area, bulb in the small path before Mithalas": 698004, + "Open Water top right area, bulb in the path from the left entrance": 698005, + "Open Water top right area, bulb in the clearing close to the bottom exit": 698006, + "Open Water top right area, bulb in the big clearing close to the save crystal": 698007, + "Open Water top right area, bulb in the big clearing to the top exit": 698008, + "Open Water top right area, first urn in the Mithalas exit": 698148, + "Open Water top right area, second urn in the Mithalas exit": 698149, + "Open Water top right area, third urn in the Mithalas exit": 698150, } locations_openwater_tr_turtle = { - "Open water top right area, bulb in the turtle room": 698009, - "Open water top right area, Transturtle": 698211, + "Open Water top right area, bulb in the turtle room": 698009, + "Open Water top right area, Transturtle": 698211, } locations_openwater_bl = { - "Open water bottom left area, bulb behind the chomper fish": 698011, - "Open water bottom left area, bulb inside the lowest fish pass": 698010, + "Open Water bottom left area, bulb behind the chomper fish": 698011, + "Open Water bottom left area, bulb inside the lowest fish pass": 698010, } locations_skeleton_path = { - "Open water skeleton path, bulb close to the right exit": 698012, - "Open water skeleton path, bulb behind the chomper fish": 698013, + "Open Water skeleton path, bulb close to the right exit": 698012, + "Open Water skeleton path, bulb behind the chomper fish": 698013, } locations_skeleton_path_sc = { - "Open water skeleton path, King skull": 698177, + "Open Water skeleton path, King Skull": 698177, } locations_arnassi = { "Arnassi Ruins, bulb in the right part": 698014, "Arnassi Ruins, bulb in the left part": 698015, "Arnassi Ruins, bulb in the center part": 698016, - "Arnassi ruins, Song plant spore on the top of the ruins": 698179, - "Arnassi ruins, Arnassi Armor": 698191, + "Arnassi Ruins, Song Plant Spore": 698179, + "Arnassi Ruins, Arnassi Armor": 698191, } locations_arnassi_path = { - "Arnassi Ruins, Arnassi statue": 698164, + "Arnassi Ruins, Arnassi Statue": 698164, "Arnassi Ruins, Transturtle": 698217, } locations_arnassi_crab_boss = { - "Arnassi ruins, Crab armor": 698187, + "Arnassi Ruins, Crab Armor": 698187, } locations_simon = { - "Kelp forest, beating Simon says": 698156, - "Simon says area, Transturtle": 698216, + "Simon Says area, beating Simon Says": 698156, + "Simon Says area, Transturtle": 698216, } locations_mithalas_city = { - "Mithalas city, first bulb in the left city part": 698030, - "Mithalas city, second bulb in the left city part": 698035, - "Mithalas city, bulb in the right part": 698031, - "Mithalas city, bulb at the top of the city": 698033, - "Mithalas city, first bulb in a broken home": 698034, - "Mithalas city, second bulb in a broken home": 698041, - "Mithalas city, bulb in the bottom left part": 698037, - "Mithalas city, first bulb in one of the homes": 698038, - "Mithalas city, second bulb in one of the homes": 698039, - "Mithalas city, first urn in one of the homes": 698123, - "Mithalas city, second urn in one of the homes": 698124, - "Mithalas city, first urn in the city reserve": 698125, - "Mithalas city, second urn in the city reserve": 698126, - "Mithalas city, third urn in the city reserve": 698127, + "Mithalas City, first bulb in the left city part": 698030, + "Mithalas City, second bulb in the left city part": 698035, + "Mithalas City, bulb in the right part": 698031, + "Mithalas City, bulb at the top of the city": 698033, + "Mithalas City, first bulb in a broken home": 698034, + "Mithalas City, second bulb in a broken home": 698041, + "Mithalas City, bulb in the bottom left part": 698037, + "Mithalas City, first bulb in one of the homes": 698038, + "Mithalas City, second bulb in one of the homes": 698039, + "Mithalas City, first urn in one of the homes": 698123, + "Mithalas City, second urn in one of the homes": 698124, + "Mithalas City, first urn in the city reserve": 698125, + "Mithalas City, second urn in the city reserve": 698126, + "Mithalas City, third urn in the city reserve": 698127, } locations_mithalas_city_top_path = { - "Mithalas city, first bulb at the end of the top path": 698032, - "Mithalas city, second bulb at the end of the top path": 698040, - "Mithalas city, bulb in the top path": 698036, - "Mithalas city, Mithalas pot": 698174, - "Mithalas city, urn in the cathedral flower tube entrance": 698128, + "Mithalas City, first bulb at the end of the top path": 698032, + "Mithalas City, second bulb at the end of the top path": 698040, + "Mithalas City, bulb in the top path": 698036, + "Mithalas City, Mithalas Pot": 698174, + "Mithalas City, urn in the Cathedral flower tube entrance": 698128, } locations_mithalas_city_fishpass = { - "Mithalas city, Doll": 698173, - "Mithalas city, urn inside a home fish pass": 698129, + "Mithalas City, Doll": 698173, + "Mithalas City, urn inside a home fish pass": 698129, } locations_cathedral_l = { - "Mithalas city castle, bulb in the flesh hole": 698042, - "Mithalas city castle, Blue banner": 698165, - "Mithalas city castle, urn in the bedroom": 698130, - "Mithalas city castle, first urn of the single lamp path": 698131, - "Mithalas city castle, second urn of the single lamp path": 698132, - "Mithalas city castle, urn in the bottom room": 698133, - "Mithalas city castle, first urn on the entrance path": 698134, - "Mithalas city castle, second urn on the entrance path": 698135, + "Mithalas City Castle, bulb in the flesh hole": 698042, + "Mithalas City Castle, Blue banner": 698165, + "Mithalas City Castle, urn in the bedroom": 698130, + "Mithalas City Castle, first urn of the single lamp path": 698131, + "Mithalas City Castle, second urn of the single lamp path": 698132, + "Mithalas City Castle, urn in the bottom room": 698133, + "Mithalas City Castle, first urn on the entrance path": 698134, + "Mithalas City Castle, second urn on the entrance path": 698135, } locations_cathedral_l_tube = { - "Mithalas castle, beating the priests": 698208, + "Mithalas City Castle, beating the Priests": 698208, } locations_cathedral_l_sc = { - "Mithalas city castle, Trident head": 698183, + "Mithalas City Castle, Trident Head": 698183, } locations_cathedral_r = { - "Mithalas cathedral, first urn in the top right room": 698136, - "Mithalas cathedral, second urn in the top right room": 698137, - "Mithalas cathedral, third urn in the top right room": 698138, - "Mithalas cathedral, urn in the flesh room with fleas": 698139, - "Mithalas cathedral, first urn in the bottom right path": 698140, - "Mithalas cathedral, second urn in the bottom right path": 698141, - "Mithalas cathedral, urn behind the flesh vein": 698142, - "Mithalas cathedral, urn in the top left eyes boss room": 698143, - "Mithalas cathedral, first urn in the path behind the flesh vein": 698144, - "Mithalas cathedral, second urn in the path behind the flesh vein": 698145, - "Mithalas cathedral, third urn in the path behind the flesh vein": 698146, - "Mithalas cathedral, one of the urns in the top right room": 698147, - "Mithalas cathedral, Mithalan Dress": 698189, - "Mithalas cathedral right area, urn below the left entrance": 698198, + "Mithalas Cathedral, first urn in the top right room": 698136, + "Mithalas Cathedral, second urn in the top right room": 698137, + "Mithalas Cathedral, third urn in the top right room": 698138, + "Mithalas Cathedral, urn in the flesh room with fleas": 698139, + "Mithalas Cathedral, first urn in the bottom right path": 698140, + "Mithalas Cathedral, second urn in the bottom right path": 698141, + "Mithalas Cathedral, urn behind the flesh vein": 698142, + "Mithalas Cathedral, urn in the top left eyes boss room": 698143, + "Mithalas Cathedral, first urn in the path behind the flesh vein": 698144, + "Mithalas Cathedral, second urn in the path behind the flesh vein": 698145, + "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146, + "Mithalas Cathedral, fourth urn in the top right room": 698147, + "Mithalas Cathedral, Mithalan Dress": 698189, + "Mithalas Cathedral right area, urn below the left entrance": 698198, } locations_cathedral_underground = { - "Cathedral underground, bulb in the center part": 698113, - "Cathedral underground, first bulb in the top left part": 698114, - "Cathedral underground, second bulb in the top left part": 698115, - "Cathedral underground, third bulb in the top left part": 698116, - "Cathedral underground, bulb close to the save cristal": 698117, - "Cathedral underground, bulb in the bottom right path": 698118, + "Cathedral Underground, bulb in the center part": 698113, + "Cathedral Underground, first bulb in the top left part": 698114, + "Cathedral Underground, second bulb in the top left part": 698115, + "Cathedral Underground, third bulb in the top left part": 698116, + "Cathedral Underground, bulb close to the save crystal": 698117, + "Cathedral Underground, bulb in the bottom right path": 698118, } locations_cathedral_boss = { @@ -250,8 +250,8 @@ class AquariaLocations: } locations_forest_tl_fp = { - "Kelp Forest top left area, bulb close to the Verse egg": 698047, - "Kelp forest top left area, Verse egg": 698158, + "Kelp Forest top left area, bulb close to the Verse Egg": 698047, + "Kelp Forest top left area, Verse Egg": 698158, } locations_forest_tr = { @@ -260,7 +260,7 @@ class AquariaLocations: "Kelp Forest top right area, bulb in the left path's big room": 698051, "Kelp Forest top right area, bulb in the left path's small room": 698052, "Kelp Forest top right area, bulb at the top of the center clearing": 698053, - "Kelp forest top right area, Black pearl": 698167, + "Kelp Forest top right area, Black Pearl": 698167, } locations_forest_tr_fp = { @@ -269,16 +269,16 @@ class AquariaLocations: locations_forest_bl = { "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp forest bottom left area, Walker baby": 698186, + "Kelp Forest bottom left area, Walker baby": 698186, "Kelp Forest bottom left area, Transturtle": 698212, } locations_forest_br = { - "Kelp forest bottom right area, Odd Container": 698168, + "Kelp Forest bottom right area, Odd Container": 698168, } locations_forest_boss = { - "Kelp forest boss area, beating Drunian God": 698204, + "Kelp Forest boss area, beating Drunian God": 698204, } locations_forest_boss_entrance = { @@ -286,7 +286,7 @@ class AquariaLocations: } locations_forest_fish_cave = { - "Kelp Forest bottom left area, Fish cave puzzle": 698207, + "Kelp Forest bottom left area, Fish Cave puzzle": 698207, } locations_forest_sprite_cave = { @@ -295,7 +295,7 @@ class AquariaLocations: locations_forest_sprite_cave_tube = { "Kelp Forest sprite cave, bulb in the second room": 698057, - "Kelp Forest Sprite Cave, Seed bag": 698176, + "Kelp Forest sprite cave, Seed Bag": 698176, } locations_mermog_cave = { @@ -307,14 +307,14 @@ class AquariaLocations: } locations_veil_tl = { - "The veil top left area, In the Li cave": 698199, - "The veil top left area, bulb under the rock in the top right path": 698078, - "The veil top left area, bulb hidden behind the blocking rock": 698076, - "The veil top left area, Transturtle": 698209, + "The Veil top left area, In Li's cave": 698199, + "The Veil top left area, bulb under the rock in the top right path": 698078, + "The Veil top left area, bulb hidden behind the blocking rock": 698076, + "The Veil top left area, Transturtle": 698209, } locations_veil_tl_fp = { - "The veil top left area, bulb inside the fish pass": 698077, + "The Veil top left area, bulb inside the fish pass": 698077, } locations_turtle_cave = { @@ -322,56 +322,56 @@ class AquariaLocations: } locations_turtle_cave_bubble = { - "Turtle cave, bulb in bubble cliff": 698000, - "Turtle cave, Urchin costume": 698193, + "Turtle cave, bulb in Bubble Cliff": 698000, + "Turtle cave, Urchin Costume": 698193, } locations_veil_tr_r = { - "The veil top right area, bulb in the middle of the wall jump cliff": 698079, - "The veil top right area, golden starfish at the bottom right of the bottom path": 698180, + "The Veil top right area, bulb in the middle of the wall jump cliff": 698079, + "The Veil top right area, Golden Starfish": 698180, } locations_veil_tr_l = { - "The veil top right area, bulb in the top of the water fall": 698080, - "The veil top right area, Transturtle": 698210, + "The Veil top right area, bulb in the top of the waterfall": 698080, + "The Veil top right area, Transturtle": 698210, } locations_veil_bl = { - "The veil bottom area, bulb in the left path": 698082, + "The Veil bottom area, bulb in the left path": 698082, } locations_veil_b_sc = { - "The veil bottom area, bulb in the spirit path": 698081, + "The Veil bottom area, bulb in the spirit path": 698081, } locations_veil_bl_fp = { - "The veil bottom area, Verse egg": 698157, + "The Veil bottom area, Verse Egg": 698157, } locations_veil_br = { - "The veil bottom area, Stone Head": 698181, + "The Veil bottom area, Stone Head": 698181, } locations_octo_cave_t = { - "Octopus cave, Dumbo Egg": 698196, + "Octopus Cave, Dumbo Egg": 698196, } locations_octo_cave_b = { - "Octopus cave, bulb in the path below the octopus cave path": 698122, + "Octopus Cave, bulb in the path below the Octopus Cave path": 698122, } locations_sun_temple_l = { - "Sun temple, bulb in the top left part": 698094, - "Sun temple, bulb in the top right part": 698095, - "Sun temple, bulb at the top of the high dark room": 698096, - "Sun temple, Golden Gear": 698171, + "Sun Temple, bulb in the top left part": 698094, + "Sun Temple, bulb in the top right part": 698095, + "Sun Temple, bulb at the top of the high dark room": 698096, + "Sun Temple, Golden Gear": 698171, } locations_sun_temple_r = { - "Sun temple, first bulb of the temple": 698091, - "Sun temple, bulb on the left part": 698092, - "Sun temple, bulb in the hidden room of the right part": 698093, - "Sun temple, Sun key": 698182, + "Sun Temple, first bulb of the temple": 698091, + "Sun Temple, bulb on the left part": 698092, + "Sun Temple, bulb in the hidden room of the right part": 698093, + "Sun Temple, Sun Key": 698182, } locations_sun_temple_boss_path = { @@ -382,13 +382,13 @@ class AquariaLocations: } locations_sun_temple_boss = { - "Sun temple boss area, beating Sun God": 698203, + "Sun Temple boss area, beating Sun God": 698203, } locations_abyss_l = { "Abyss left area, bulb in hidden path room": 698024, "Abyss left area, bulb in the right part": 698025, - "Abyss left area, Glowing seed": 698166, + "Abyss left area, Glowing Seed": 698166, "Abyss left area, Glowing Plant": 698172, } @@ -405,87 +405,87 @@ class AquariaLocations: } locations_ice_cave = { - "Ice cave, bulb in the room to the right": 698083, - "Ice cave, First bulbs in the top exit room": 698084, - "Ice cave, Second bulbs in the top exit room": 698085, - "Ice cave, third bulbs in the top exit room": 698086, - "Ice cave, bulb in the left room": 698087, + "Ice Cave, bulb in the room to the right": 698083, + "Ice Cave, first bulb in the top exit room": 698084, + "Ice Cave, second bulb in the top exit room": 698085, + "Ice Cave, third bulb in the top exit room": 698086, + "Ice Cave, bulb in the left room": 698087, } locations_bubble_cave = { - "Bubble cave, bulb in the left cave wall": 698089, - "Bubble cave, bulb in the right cave wall (behind the ice cristal)": 698090, + "Bubble Cave, bulb in the left cave wall": 698089, + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)": 698090, } locations_bubble_cave_boss = { - "Bubble cave, Verse egg": 698161, + "Bubble Cave, Verse Egg": 698161, } locations_king_jellyfish_cave = { - "King Jellyfish cave, bulb in the right path from King Jelly": 698088, - "King Jellyfish cave, Jellyfish Costume": 698188, + "King Jellyfish Cave, bulb in the right path from King Jelly": 698088, + "King Jellyfish Cave, Jellyfish Costume": 698188, } locations_whale = { - "The whale, Verse egg": 698159, + "The Whale, Verse Egg": 698159, } locations_sunken_city_r = { - "Sunken city right area, crate close to the save cristal": 698154, - "Sunken city right area, crate in the left bottom room": 698155, + "Sunken City right area, crate close to the save crystal": 698154, + "Sunken City right area, crate in the left bottom room": 698155, } locations_sunken_city_l = { - "Sunken city left area, crate in the little pipe room": 698151, - "Sunken city left area, crate close to the save cristal": 698152, - "Sunken city left area, crate before the bedroom": 698153, + "Sunken City left area, crate in the little pipe room": 698151, + "Sunken City left area, crate close to the save crystal": 698152, + "Sunken City left area, crate before the bedroom": 698153, } locations_sunken_city_l_bedroom = { - "Sunken city left area, Girl Costume": 698192, + "Sunken City left area, Girl Costume": 698192, } locations_sunken_city_boss = { - "Sunken city, bulb on the top of the boss area (boiler room)": 698043, + "Sunken City, bulb on top of the boss area": 698043, } locations_body_c = { - "The body center area, breaking li cage": 698201, - "The body main area, bulb on the main path blocking tube": 698097, + "The Body center area, breaking Li's cage": 698201, + "The Body main area, bulb on the main path blocking tube": 698097, } locations_body_l = { - "The body left area, first bulb in the top face room": 698066, - "The body left area, second bulb in the top face room": 698069, - "The body left area, bulb below the water stream": 698067, - "The body left area, bulb in the top path to the top face room": 698068, - "The body left area, bulb in the bottom face room": 698070, + "The Body left area, first bulb in the top face room": 698066, + "The Body left area, second bulb in the top face room": 698069, + "The Body left area, bulb below the water stream": 698067, + "The Body left area, bulb in the top path to the top face room": 698068, + "The Body left area, bulb in the bottom face room": 698070, } locations_body_rt = { - "The body right area, bulb in the top face room": 698100, + "The Body right area, bulb in the top face room": 698100, } locations_body_rb = { - "The body right area, bulb in the top path to the bottom face room": 698098, - "The body right area, bulb in the bottom face room": 698099, + "The Body right area, bulb in the top path to the bottom face room": 698098, + "The Body right area, bulb in the bottom face room": 698099, } locations_body_b = { - "The body bottom area, bulb in the Jelly Zap room": 698101, - "The body bottom area, bulb in the nautilus room": 698102, - "The body bottom area, Mutant Costume": 698190, + "The Body bottom area, bulb in the Jelly Zap room": 698101, + "The Body bottom area, bulb in the nautilus room": 698102, + "The Body bottom area, Mutant Costume": 698190, } locations_final_boss_tube = { - "Final boss area, first bulb in the turtle room": 698103, - "Final boss area, second bulbs in the turtle room": 698104, - "Final boss area, third bulbs in the turtle room": 698105, - "Final boss area, Transturtle": 698215, + "Final Boss area, first bulb in the turtle room": 698103, + "Final Boss area, second bulb in the turtle room": 698104, + "Final Boss area, third bulb in the turtle room": 698105, + "Final Boss area, Transturtle": 698215, } locations_final_boss = { - "Final boss area, bulb in the boss third form room": 698106, + "Final Boss area, bulb in the boss third form room": 698106, } diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 9a49e915b9..4c795d3508 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -113,7 +113,7 @@ class BindSongNeededToGetUnderRockBulb(Toggle): class UnconfineHomeWater(Choice): """ - Open the way out of the Home water area so that Naija can go to open water and beyond without the bind song. + Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. """ display_name = "Unconfine Home Water Area" option_off = 0 diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 5956e0ca84..f2f85749f3 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -36,8 +36,8 @@ def _has_li(state:CollectionState, player: int) -> bool: def _has_damaging_item(state:CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby nautilus", - "Baby piranha", "Baby blaster"}, player) + return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", + "Baby Piranha", "Baby Blaster"}, player) def _has_shield_song(state:CollectionState, player: int) -> bool: @@ -72,7 +72,7 @@ def _has_sun_form(state:CollectionState, player: int) -> bool: def _has_light(state:CollectionState, player: int) -> bool: """`player` in `state` has the light item""" - return state.has("Baby dumbo", player) or _has_sun_form(state, player) + return state.has("Baby Dumbo", player) or _has_sun_form(state, player) def _has_dual_form(state:CollectionState, player: int) -> bool: @@ -237,26 +237,26 @@ class AquariaRegions: AquariaLocations.locations_home_water_nautilus) self.home_water_transturtle = self.__add_region("Home Water, turtle room", AquariaLocations.locations_home_water_transturtle) - self.naija_home = self.__add_region("Naija's home", AquariaLocations.locations_naija_home) - self.song_cave = self.__add_region("Song cave", AquariaLocations.locations_song_cave) + self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) + self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) def __create_energy_temple(self) -> None: """ Create the `energy_temple_*` regions """ - self.energy_temple_1 = self.__add_region("Energy temple first area", + self.energy_temple_1 = self.__add_region("Energy Temple first area", AquariaLocations.locations_energy_temple_1) - self.energy_temple_2 = self.__add_region("Energy temple second area", + self.energy_temple_2 = self.__add_region("Energy Temple second area", AquariaLocations.locations_energy_temple_2) - self.energy_temple_3 = self.__add_region("Energy temple third area", + self.energy_temple_3 = self.__add_region("Energy Temple third area", AquariaLocations.locations_energy_temple_3) - self.energy_temple_altar = self.__add_region("Energy temple bottom entrance", + self.energy_temple_altar = self.__add_region("Energy Temple bottom entrance", AquariaLocations.locations_energy_temple_altar) - self.energy_temple_boss = self.__add_region("Energy temple fallen God room", + self.energy_temple_boss = self.__add_region("Energy Temple fallen God room", AquariaLocations.locations_energy_temple_boss) - self.energy_temple_idol = self.__add_region("Energy temple Idol room", + self.energy_temple_idol = self.__add_region("Energy Temple Idol room", AquariaLocations.locations_energy_temple_idol) - self.energy_temple_blaster_room = self.__add_region("Energy temple blaster room", + self.energy_temple_blaster_room = self.__add_region("Energy Temple blaster room", AquariaLocations.locations_energy_temple_blaster_room) def __create_openwater(self) -> None: @@ -264,18 +264,18 @@ class AquariaRegions: Create the `openwater_*`, `skeleton_path`, `arnassi*` and `simon` regions """ - self.openwater_tl = self.__add_region("Open water top left area", + self.openwater_tl = self.__add_region("Open Water top left area", AquariaLocations.locations_openwater_tl) - self.openwater_tr = self.__add_region("Open water top right area", + self.openwater_tr = self.__add_region("Open Water top right area", AquariaLocations.locations_openwater_tr) - self.openwater_tr_turtle = self.__add_region("Open water top right area, turtle room", + self.openwater_tr_turtle = self.__add_region("Open Water top right area, turtle room", AquariaLocations.locations_openwater_tr_turtle) - self.openwater_bl = self.__add_region("Open water bottom left area", + self.openwater_bl = self.__add_region("Open Water bottom left area", AquariaLocations.locations_openwater_bl) - self.openwater_br = self.__add_region("Open water bottom right area", None) - self.skeleton_path = self.__add_region("Open water skeleton path", + self.openwater_br = self.__add_region("Open Water bottom right area", None) + self.skeleton_path = self.__add_region("Open Water skeleton path", AquariaLocations.locations_skeleton_path) - self.skeleton_path_sc = self.__add_region("Open water skeleton path spirit cristal", + self.skeleton_path_sc = self.__add_region("Open Water skeleton path spirit crystal", AquariaLocations.locations_skeleton_path_sc) self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", @@ -287,20 +287,20 @@ class AquariaRegions: """ Create the `mithalas_city*` and `cathedral_*` regions """ - self.mithalas_city = self.__add_region("Mithalas city", + self.mithalas_city = self.__add_region("Mithalas City", AquariaLocations.locations_mithalas_city) - self.mithalas_city_fishpass = self.__add_region("Mithalas city fish pass", + self.mithalas_city_fishpass = self.__add_region("Mithalas City fish pass", AquariaLocations.locations_mithalas_city_fishpass) - self.mithalas_city_top_path = self.__add_region("Mithalas city top path", + self.mithalas_city_top_path = self.__add_region("Mithalas City top path", AquariaLocations.locations_mithalas_city_top_path) self.cathedral_l = self.__add_region("Mithalas castle", AquariaLocations.locations_cathedral_l) self.cathedral_l_tube = self.__add_region("Mithalas castle, plant tube entrance", AquariaLocations.locations_cathedral_l_tube) - self.cathedral_l_sc = self.__add_region("Mithalas castle spirit cristal", + self.cathedral_l_sc = self.__add_region("Mithalas castle spirit crystal", AquariaLocations.locations_cathedral_l_sc) self.cathedral_r = self.__add_region("Mithalas Cathedral", AquariaLocations.locations_cathedral_r) - self.cathedral_underground = self.__add_region("Mithalas Cathedral underground area", + self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area", AquariaLocations.locations_cathedral_underground) self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", AquariaLocations.locations_cathedral_boss) @@ -310,73 +310,73 @@ class AquariaRegions: """ Create the `forest_*` dans `mermog_cave` regions """ - self.forest_tl = self.__add_region("Kelp forest top left area", + self.forest_tl = self.__add_region("Kelp Forest top left area", AquariaLocations.locations_forest_tl) - self.forest_tl_fp = self.__add_region("Kelp forest top left area fish pass", + self.forest_tl_fp = self.__add_region("Kelp Forest top left area fish pass", AquariaLocations.locations_forest_tl_fp) - self.forest_tr = self.__add_region("Kelp forest top right area", + self.forest_tr = self.__add_region("Kelp Forest top right area", AquariaLocations.locations_forest_tr) - self.forest_tr_fp = self.__add_region("Kelp forest top right area fish pass", + self.forest_tr_fp = self.__add_region("Kelp Forest top right area fish pass", AquariaLocations.locations_forest_tr_fp) - self.forest_bl = self.__add_region("Kelp forest bottom left area", + self.forest_bl = self.__add_region("Kelp Forest bottom left area", AquariaLocations.locations_forest_bl) - self.forest_br = self.__add_region("Kelp forest bottom right area", + self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) - self.forest_sprite_cave = self.__add_region("Kelp forest spirit cave", + self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", AquariaLocations.locations_forest_sprite_cave) - self.forest_sprite_cave_tube = self.__add_region("Kelp forest spirit cave after the plant tube", + self.forest_sprite_cave_tube = self.__add_region("Kelp Forest spirit cave after the plant tube", AquariaLocations.locations_forest_sprite_cave_tube) - self.forest_boss = self.__add_region("Kelp forest Drunian God room", + self.forest_boss = self.__add_region("Kelp Forest Drunian God room", AquariaLocations.locations_forest_boss) - self.forest_boss_entrance = self.__add_region("Kelp forest Drunian God room entrance", + self.forest_boss_entrance = self.__add_region("Kelp Forest Drunian God room entrance", AquariaLocations.locations_forest_boss_entrance) - self.mermog_cave = self.__add_region("Kelp forest Mermog cave", + self.mermog_cave = self.__add_region("Kelp Forest Mermog cave", AquariaLocations.locations_mermog_cave) - self.mermog_boss = self.__add_region("Kelp forest Mermog cave boss", + self.mermog_boss = self.__add_region("Kelp Forest Mermog cave boss", AquariaLocations.locations_mermog_boss) - self.forest_fish_cave = self.__add_region("Kelp forest fish cave", + self.forest_fish_cave = self.__add_region("Kelp Forest fish cave", AquariaLocations.locations_forest_fish_cave) - self.simon = self.__add_region("Kelp forest, Simon's room", AquariaLocations.locations_simon) + self.simon = self.__add_region("Kelp Forest, Simon's room", AquariaLocations.locations_simon) def __create_veil(self) -> None: """ Create the `veil_*`, `octo_cave` and `turtle_cave` regions """ - self.veil_tl = self.__add_region("The veil top left area", AquariaLocations.locations_veil_tl) - self.veil_tl_fp = self.__add_region("The veil top left area fish pass", + self.veil_tl = self.__add_region("The Veil top left area", AquariaLocations.locations_veil_tl) + self.veil_tl_fp = self.__add_region("The Veil top left area fish pass", AquariaLocations.locations_veil_tl_fp) - self.turtle_cave = self.__add_region("The veil top left area, turtle cave", + self.turtle_cave = self.__add_region("The Veil top left area, turtle cave", AquariaLocations.locations_turtle_cave) - self.turtle_cave_bubble = self.__add_region("The veil top left area, turtle cave bubble cliff", + self.turtle_cave_bubble = self.__add_region("The Veil top left area, turtle cave Bubble Cliff", AquariaLocations.locations_turtle_cave_bubble) - self.veil_tr_l = self.__add_region("The veil top right area, left of temple", + self.veil_tr_l = self.__add_region("The Veil top right area, left of temple", AquariaLocations.locations_veil_tr_l) - self.veil_tr_r = self.__add_region("The veil top right area, right of temple", + self.veil_tr_r = self.__add_region("The Veil top right area, right of temple", AquariaLocations.locations_veil_tr_r) - self.octo_cave_t = self.__add_region("Octopus cave top entrance", + self.octo_cave_t = self.__add_region("Octopus Cave top entrance", AquariaLocations.locations_octo_cave_t) - self.octo_cave_b = self.__add_region("Octopus cave bottom entrance", + self.octo_cave_b = self.__add_region("Octopus Cave bottom entrance", AquariaLocations.locations_octo_cave_b) - self.veil_bl = self.__add_region("The veil bottom left area", + self.veil_bl = self.__add_region("The Veil bottom left area", AquariaLocations.locations_veil_bl) - self.veil_b_sc = self.__add_region("The veil bottom spirit cristal area", + self.veil_b_sc = self.__add_region("The Veil bottom spirit crystal area", AquariaLocations.locations_veil_b_sc) - self.veil_bl_fp = self.__add_region("The veil bottom left area, in the sunken ship", + self.veil_bl_fp = self.__add_region("The Veil bottom left area, in the sunken ship", AquariaLocations.locations_veil_bl_fp) - self.veil_br = self.__add_region("The veil bottom right area", + self.veil_br = self.__add_region("The Veil bottom right area", AquariaLocations.locations_veil_br) def __create_sun_temple(self) -> None: """ Create the `sun_temple*` regions """ - self.sun_temple_l = self.__add_region("Sun temple left area", + self.sun_temple_l = self.__add_region("Sun Temple left area", AquariaLocations.locations_sun_temple_l) - self.sun_temple_r = self.__add_region("Sun temple right area", + self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) - self.sun_temple_boss_path = self.__add_region("Sun temple before boss area", + self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", AquariaLocations.locations_sun_temple_boss_path) - self.sun_temple_boss = self.__add_region("Sun temple boss area", + self.sun_temple_boss = self.__add_region("Sun Temple boss area", AquariaLocations.locations_sun_temple_boss) def __create_abyss(self) -> None: @@ -388,9 +388,9 @@ class AquariaRegions: AquariaLocations.locations_abyss_l) self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb) self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) - self.ice_cave = self.__add_region("Ice cave", AquariaLocations.locations_ice_cave) - self.bubble_cave = self.__add_region("Bubble cave", AquariaLocations.locations_bubble_cave) - self.bubble_cave_boss = self.__add_region("Bubble cave boss area", AquariaLocations.locations_bubble_cave_boss) + self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) + self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) + self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave", AquariaLocations.locations_king_jellyfish_cave) self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale) @@ -400,35 +400,35 @@ class AquariaRegions: """ Create the `sunken_city_*` regions """ - self.sunken_city_l = self.__add_region("Sunken city left area", + self.sunken_city_l = self.__add_region("Sunken City left area", AquariaLocations.locations_sunken_city_l) - self.sunken_city_l_bedroom = self.__add_region("Sunken city left area, bedroom", + self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom", AquariaLocations.locations_sunken_city_l_bedroom) - self.sunken_city_r = self.__add_region("Sunken city right area", + self.sunken_city_r = self.__add_region("Sunken City right area", AquariaLocations.locations_sunken_city_r) - self.sunken_city_boss = self.__add_region("Sunken city boss area", + self.sunken_city_boss = self.__add_region("Sunken City boss area", AquariaLocations.locations_sunken_city_boss) def __create_body(self) -> None: """ Create the `body_*` and `final_boss* regions """ - self.body_c = self.__add_region("The body center area", + self.body_c = self.__add_region("The Body center area", AquariaLocations.locations_body_c) - self.body_l = self.__add_region("The body left area", + self.body_l = self.__add_region("The Body left area", AquariaLocations.locations_body_l) - self.body_rt = self.__add_region("The body right area, top path", + self.body_rt = self.__add_region("The Body right area, top path", AquariaLocations.locations_body_rt) - self.body_rb = self.__add_region("The body right area, bottom path", + self.body_rb = self.__add_region("The Body right area, bottom path", AquariaLocations.locations_body_rb) - self.body_b = self.__add_region("The body bottom area", + self.body_b = self.__add_region("The Body bottom area", AquariaLocations.locations_body_b) - self.final_boss_loby = self.__add_region("The body, before final boss", None) - self.final_boss_tube = self.__add_region("The body, final boss area turtle room", + self.final_boss_loby = self.__add_region("The Body, before final boss", None) + self.final_boss_tube = self.__add_region("The Body, final boss area turtle room", AquariaLocations.locations_final_boss_tube) - self.final_boss = self.__add_region("The body, final boss", + self.final_boss = self.__add_region("The Body, final boss", AquariaLocations.locations_final_boss) - self.final_boss_end = self.__add_region("The body, final boss area", None) + self.final_boss_end = self.__add_region("The Body, final boss area", None) def __connect_one_way_regions(self, source_name: str, destination_name: str, source_region: Region, @@ -455,99 +455,99 @@ class AquariaRegions: """ Connect entrances of the different regions around `home_water` """ - self.__connect_regions("Menu", "Verse cave right area", + self.__connect_regions("Menu", "Verse Cave right area", self.menu, self.verse_cave_r) - self.__connect_regions("Verse cave left area", "Verse cave right area", + self.__connect_regions("Verse Cave left area", "Verse Cave right area", self.verse_cave_l, self.verse_cave_r) - self.__connect_regions("Verse cave", "Home water", self.verse_cave_l, self.home_water) + self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) self.__connect_regions("Home Water", "Haija's home", self.home_water, self.naija_home) - self.__connect_regions("Home Water", "Song cave", self.home_water, self.song_cave) - self.__connect_regions("Home Water", "Home water, nautilus nest", + self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) + self.__connect_regions("Home Water", "Home Water, nautilus nest", self.home_water, self.home_water_nautilus, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Home water transturtle room", + self.__connect_regions("Home Water", "Home Water transturtle room", self.home_water, self.home_water_transturtle) - self.__connect_regions("Home Water", "Energy temple first area", + self.__connect_regions("Home Water", "Energy Temple first area", self.home_water, self.energy_temple_1, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Home Water", "Energy temple_altar", + self.__connect_regions("Home Water", "Energy Temple_altar", self.home_water, self.energy_temple_altar, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple second area", + self.__connect_regions("Energy Temple first area", "Energy Temple second area", self.energy_temple_1, self.energy_temple_2, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple idol room", + self.__connect_regions("Energy Temple first area", "Energy Temple idol room", self.energy_temple_1, self.energy_temple_idol, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Energy temple idol room", "Energy temple boss area", + self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", self.energy_temple_idol, self.energy_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple first area", "Energy temple boss area", + self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", self.energy_temple_1, self.energy_temple_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Energy temple boss area", "Energy temple first area", + self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", self.energy_temple_boss, self.energy_temple_1, lambda state: _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple second area", "Energy temple third area", + self.__connect_regions("Energy Temple second area", "Energy Temple third area", self.energy_temple_2, self.energy_temple_3, lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple boss area", "Energy temple blaster room", + self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", self.energy_temple_boss, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Energy temple first area", "Energy temple blaster room", + self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", self.energy_temple_1, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player) and _has_beast_form(state, self.player)) - self.__connect_regions("Home Water", "Open water top left area", + self.__connect_regions("Home Water", "Open Water top left area", self.home_water, self.openwater_tl) def __connect_open_water_regions(self) -> None: """ Connect entrances of the different regions around open water """ - self.__connect_regions("Open water top left area", "Open water top right area", + self.__connect_regions("Open Water top left area", "Open Water top right area", self.openwater_tl, self.openwater_tr) - self.__connect_regions("Open water top left area", "Open water bottom left area", + self.__connect_regions("Open Water top left area", "Open Water bottom left area", self.openwater_tl, self.openwater_bl) - self.__connect_regions("Open water top left area", "forest bottom right area", + self.__connect_regions("Open Water top left area", "forest bottom right area", self.openwater_tl, self.forest_br) - self.__connect_regions("Open water top right area", "Open water top right area, turtle room", + self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", self.openwater_tr, self.openwater_tr_turtle, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water top right area", "Open water bottom right area", + self.__connect_regions("Open Water top right area", "Open Water bottom right area", self.openwater_tr, self.openwater_br) - self.__connect_regions("Open water top right area", "Mithalas city", + self.__connect_regions("Open Water top right area", "Mithalas City", self.openwater_tr, self.mithalas_city) - self.__connect_regions("Open water top right area", "Veil bottom left area", + self.__connect_regions("Open Water top right area", "Veil bottom left area", self.openwater_tr, self.veil_bl) - self.__connect_one_way_regions("Open water top right area", "Veil bottom right", + self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", self.openwater_tr, self.veil_br, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Veil bottom right", "Open water top right area", + self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", self.veil_br, self.openwater_tr, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Open water bottom left area", "Open water bottom right area", + self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", self.openwater_bl, self.openwater_br) - self.__connect_regions("Open water bottom left area", "Skeleton path", + self.__connect_regions("Open Water bottom left area", "Skeleton path", self.openwater_bl, self.skeleton_path) - self.__connect_regions("Abyss left area", "Open water bottom left area", + self.__connect_regions("Abyss left area", "Open Water bottom left area", self.abyss_l, self.openwater_bl) self.__connect_regions("Skeleton path", "skeleton_path_sc", self.skeleton_path, self.skeleton_path_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Open water bottom right area", + self.__connect_regions("Abyss right area", "Open Water bottom right area", self.abyss_r, self.openwater_br) - self.__connect_one_way_regions("Open water bottom right area", "Arnassi", + self.__connect_one_way_regions("Open Water bottom right area", "Arnassi", self.openwater_br, self.arnassi, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Arnassi", "Open water bottom right area", + self.__connect_one_way_regions("Arnassi", "Open Water bottom right area", self.arnassi, self.openwater_br) self.__connect_regions("Arnassi", "Arnassi path", self.arnassi, self.arnassi_path) @@ -562,23 +562,23 @@ class AquariaRegions: """ Connect entrances of the different regions around Mithalas """ - self.__connect_one_way_regions("Mithalas city", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", self.mithalas_city, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city_top_path", "Mithalas city", + self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", self.mithalas_city_top_path, self.mithalas_city) - self.__connect_regions("Mithalas city", "Mithalas city home with fishpass", + self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) - self.__connect_regions("Mithalas city", "Mithalas castle", + self.__connect_regions("Mithalas City", "Mithalas castle", self.mithalas_city, self.cathedral_l, lambda state: _has_fish_form(state, self.player)) - self.__connect_one_way_regions("Mithalas city top path", "Mithalas castle, flower tube", + self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", self.mithalas_city_top_path, self.cathedral_l_tube, lambda state: _has_nature_form(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas city top path", + self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", self.cathedral_l_tube, self.mithalas_city_top_path, lambda state: _has_beast_form(state, self.player) and @@ -690,22 +690,22 @@ class AquariaRegions: self.veil_tl, self.veil_tr_r) self.__connect_regions("Veil top left area", "Turtle cave", self.veil_tl, self.turtle_cave) - self.__connect_regions("Turtle cave", "Turtle cave bubble cliff", + self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", self.turtle_cave, self.turtle_cave_bubble, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Veil right of sun temple", "Sun temple right area", + self.__connect_regions("Veil right of sun temple", "Sun Temple right area", self.veil_tr_r, self.sun_temple_r) - self.__connect_regions("Sun temple right area", "Sun temple left area", + self.__connect_regions("Sun Temple right area", "Sun Temple left area", self.sun_temple_r, self.sun_temple_l, lambda state: _has_bind_song(state, self.player)) - self.__connect_regions("Sun temple left area", "Veil left of sun temple", + self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) - self.__connect_regions("Sun temple left area", "Sun temple before boss area", + self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", self.sun_temple_l, self.sun_temple_boss_path) - self.__connect_regions("Sun temple before boss area", "Sun temple boss area", + self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Sun temple boss area", "Veil left of sun temple", + self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", self.sun_temple_boss, self.veil_tr_l) self.__connect_regions("Veil left of sun temple", "Octo cave top path", self.veil_tr_l, self.octo_cave_t, @@ -724,7 +724,7 @@ class AquariaRegions: self.__connect_regions("Abyss left area", "Abyss bottom of left area", self.abyss_l, self.abyss_lb, lambda state: _has_nature_form(state, self.player)) - self.__connect_regions("Abyss left bottom area", "Sunken city right area", + self.__connect_regions("Abyss left bottom area", "Sunken City right area", self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) self.__connect_one_way_regions("Abyss left bottom area", "Body center area", @@ -748,13 +748,13 @@ class AquariaRegions: _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Abyss right area", "Ice cave", + self.__connect_regions("Abyss right area", "Ice Cave", self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Bubble cave", + self.__connect_regions("Abyss right area", "Bubble Cave", self.ice_cave, self.bubble_cave, lambda state: _has_beast_form(state, self.player)) - self.__connect_regions("Bubble cave boss area", "Bubble cave", + self.__connect_regions("Bubble Cave boss area", "Bubble Cave", self.bubble_cave, self.bubble_cave_boss, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) ) @@ -763,12 +763,12 @@ class AquariaRegions: """ Connect entrances of the different regions around The Sunken City """ - self.__connect_regions("Sunken city right area", "Sunken city left area", + self.__connect_regions("Sunken City right area", "Sunken City left area", self.sunken_city_r, self.sunken_city_l) - self.__connect_regions("Sunken city left area", "Sunken city bedroom", + self.__connect_regions("Sunken City left area", "Sunken City bedroom", self.sunken_city_l, self.sunken_city_l_bedroom, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Sunken city left area", "Sunken city boss area", + self.__connect_regions("Sunken City left area", "Sunken City boss area", self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player) and @@ -776,7 +776,7 @@ class AquariaRegions: def __connect_body_regions(self) -> None: """ - Connect entrances of the different regions around The body + Connect entrances of the different regions around The Body """ self.__connect_regions("Body center area", "Body left area", self.body_c, self.body_l) @@ -787,13 +787,13 @@ class AquariaRegions: self.__connect_regions("Body center area", "Body bottom area", self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Body bottom area", "Final boss area", + self.__connect_regions("Body bottom area", "Final Boss area", self.body_b, self.final_boss_loby, lambda state: _has_dual_form(state, self.player)) - self.__connect_regions("Before Final boss", "Final boss tube", + self.__connect_regions("Before Final Boss", "Final Boss tube", self.final_boss_loby, self.final_boss_tube, lambda state: _has_nature_form(state, self.player)) - self.__connect_one_way_regions("Before Final boss", "Final boss", + self.__connect_one_way_regions("Before Final Boss", "Final Boss", self.final_boss_loby, self.final_boss, lambda state: _has_energy_form(state, self.player) and _has_dual_form(state, self.player) and @@ -814,7 +814,7 @@ class AquariaRegions: def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region) -> None: - """Connect the Arnassi ruins transturtle to another one""" + """Connect the Arnassi Ruins transturtle to another one""" self.__connect_one_way_regions(item_source, item_target, region_source, region_target, lambda state: state.has(item_target, self.player) and _has_fish_form(state, self.player)) @@ -825,25 +825,25 @@ class AquariaRegions: self.__connect_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_transturtle(item, "Transturtle Simon says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi ruins", region, self.arnassi_path, - lambda state: state.has("Transturtle Arnassi ruins", self.player) and + self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) + self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path, + lambda state: state.has("Transturtle Arnassi Ruins", self.player) and _has_fish_form(state, self.player)) def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None: - """Connect the Arnassi ruins transturtle to all others""" + """Connect the Arnassi Ruins transturtle to all others""" self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Home water", region, self.home_water_transturtle) + self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_arnassi_path_transturtle(item, "Transturtle Simon says", region, self.simon) + self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" @@ -851,11 +851,11 @@ class AquariaRegions: self._connect_transturtle_to_other("Transturtle Veil top right", self.veil_tr_l) self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) - self._connect_transturtle_to_other("Transturtle Home water", self.home_water_transturtle) + self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r) self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) - self._connect_transturtle_to_other("Transturtle Simon says", self.simon) - self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi ruins", self.arnassi_path) + self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) + self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path) def connect_regions(self) -> None: """ @@ -907,7 +907,7 @@ class AquariaRegions: def __add_event_mini_bosses(self) -> None: """ - Add every mini bosses (excluding Energy statue and Simon says) + Add every mini bosses (excluding Energy Statue and Simon Says) events to the `world` """ self.__add_event_location(self.home_water_nautilus, @@ -967,100 +967,100 @@ class AquariaRegions: def __adjusting_urns_rules(self) -> None: """Since Urns need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Open water top right area, first urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, second urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open water top right area, third urn in the Mithalas exit", self.player), + add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in one of the homes", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in one of the homes", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, first urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, first urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, second urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, second urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, third urn in the city reserve", self.player), + add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn in the cathedral flower tube entrance", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bedroom", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn of the single lamp path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn of the single lamp path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, urn in the bottom room", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, first urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, first urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city castle, second urn on the entrance path", self.player), + add_rule(self.multiworld.get_location("Mithalas City Castle, second urn on the entrance path", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas city, urn inside a home fish pass", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_crates_rules(self) -> None: """Since Crate need to be broken, add a damaging item to rules""" - add_rule(self.multiworld.get_location("Sunken city right area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city right area, crate in the left bottom room", self.player), + add_rule(self.multiworld.get_location("Sunken City right area, crate in the left bottom room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate in the little pipe room", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate in the little pipe room", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate close to the save cristal", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate close to the save crystal", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Sunken city left area, crate before the bedroom", self.player), + add_rule(self.multiworld.get_location("Sunken City left area, crate before the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) def __adjusting_soup_rules(self) -> None: """ Modify rules for location that need soup """ - add_rule(self.multiworld.get_location("Turtle cave, Urchin costume", self.player), + add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", self.player), + add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ - add_rule(self.multiworld.get_location("Home water, bulb under the rock in the left path from the verse cave", + add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave left area, bulb under the rock at the end of the path", + add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb under the rock at the right of the main path", + add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock in the path to the singing statues", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, bulb under the rock close to the song door", + add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple second area, bulb under the rock", + add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the right path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Open water top left area, bulb under the rock in the left path", + add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb under the rock in the top right path", + add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: - add_rule(self.multiworld.get_location("Kelp forest top right area, Black pearl", self.player), + add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom right area, Odd Container", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) @@ -1070,103 +1070,103 @@ class AquariaRegions: lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Home water to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Simon says to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Arnassi ruins to Transturtle Abyss right", self.player), + add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom right area to Abyss right area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom right area to Abyss right area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Open water bottom left area to Abyss left area", self.player), + add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple left area to Sun temple right area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun temple right area to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun temple left area", self.player), + add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) def __adjusting_manual_rules(self) -> None: - add_rule(self.multiworld.get_location("Mithalas cathedral, Mithalan Dress", self.player), + add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Open water bottom left area, bulb inside the lowest fish pass", self.player), + add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp forest bottom left area, Walker baby", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player), lambda state: _has_spirit_form(state, self.player)) - add_rule(self.multiworld.get_location("The veil top left area, bulb hidden behind the blocking rock", self.player), + add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Anemone seed", self.player), + add_rule(self.multiworld.get_location("Song Cave, Anemone Seed", self.player), lambda state: _has_nature_form(state, self.player)) - add_rule(self.multiworld.get_location("Song cave, Verse egg", self.player), + add_rule(self.multiworld.get_location("Song Cave, Verse Egg", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Verse cave right area, Big Seed", self.player), + add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Song plant spore on the top of the ruins", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), lambda state: _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Energy temple first area, bulb in the bottom room blocked by a rock", + add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", self.player), lambda state: _has_energy_form(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the bottom left room", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Home water, bulb in the path below Nautilus Prime", self.player), + add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), lambda state: _has_bind_song(state, self.player)) - add_rule(self.multiworld.get_location("Naija's home, bulb after the energy door", self.player), + add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), lambda state: _has_energy_form(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) - add_rule(self.multiworld.get_location("Arnassi ruins, Arnassi Armor", self.player), + add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), lambda state: _has_fish_form(state, self.player) and _has_spirit_form(state, self.player)) def __no_progression_hard_or_hidden_location(self) -> None: - self.multiworld.get_location("Energy temple boss area, Fallen god tooth", + self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Cathedral boss area, beating Mithalan God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest boss area, beating Drunian God", + self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple boss area, beating Sun God", + self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sunken city, bulb on the top of the boss area (boiler room)", + self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Home water, Nautilus Egg", + self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Energy temple blaster room, Blaster egg", + self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Mithalas castle, beating the priests", + self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Octopus cave, Dumbo Egg", + self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, bulb in the right path from King Jelly", + self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("King Jellyfish cave, Jellyfish Costume", + self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Final boss area, bulb in the boss third form room", + self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, first cliff bulb", @@ -1175,34 +1175,34 @@ class AquariaRegions: self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The veil top right area, bulb in the top of the water fall", + self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the left cave wall", + self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, bulb in the right cave wall (behind the ice cristal)", + self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Bubble cave, Verse egg", + self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp forest bottom left area, Walker baby", + self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, Sun key", + self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The body bottom area, Mutant Costume", + self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Sun temple, bulb in the hidden room of the right part", + self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Arnassi ruins, Arnassi Armor", + self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression @@ -1220,19 +1220,19 @@ class AquariaRegions: self.__adjusting_under_rock_location() if options.mini_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_mini_bosses(state, self.player)) if options.big_bosses_to_beat.value > 0: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_big_bosses(state, self.player)) if options.objective.value == 1: - add_rule(self.multiworld.get_entrance("Before Final boss to Final boss", self.player), + add_rule(self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player), lambda state: _has_secrets(state, self.player)) if options.unconfine_home_water.value in [0, 1]: - add_rule(self.multiworld.get_entrance("Home Water to Home water transturtle room", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Home Water transturtle room", self.player), lambda state: _has_bind_song(state, self.player)) if options.unconfine_home_water.value in [0, 2]: - add_rule(self.multiworld.get_entrance("Home Water to Open water top left area", self.player), + add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) if options.early_energy_form: self.multiworld.early_items[self.player]["Energy form"] = 1 diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 7c92d33a9c..3c0cc3bded 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -71,9 +71,9 @@ class AquariaWorld(World): item_name_groups = { "Damage": {"Energy form", "Nature form", "Beast form", - "Li and Li song", "Baby nautilus", "Baby piranha", - "Baby blaster"}, - "Light": {"Sun form", "Baby dumbo"} + "Li and Li song", "Baby Nautilus", "Baby Piranha", + "Baby Blaster"}, + "Light": {"Sun form", "Baby Dumbo"} } """Grouping item make it easier to find them""" @@ -152,20 +152,20 @@ class AquariaWorld(World): precollected = [item.name for item in self.multiworld.precollected_items[self.player]] if self.options.turtle_randomizer.value > 0: if self.options.turtle_randomizer.value == 2: - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) else: - self.__pre_fill_item("Transturtle Veil top left", "The veil top left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Veil top right", "The veil top right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Open Water top right", "Open water top right area, Transturtle", + self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Home water", "Home water, Transturtle", precollected) + self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected) self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) - self.__pre_fill_item("Transturtle Final Boss", "Final boss area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) # The last two are inverted because in the original game, they are special turtle that communicate directly - self.__pre_fill_item("Transturtle Simon says", "Arnassi Ruins, Transturtle", precollected) - self.__pre_fill_item("Transturtle Arnassi ruins", "Simon says area, Transturtle", precollected) + self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) + self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): if name in precollected: precollected.remove(name) diff --git a/worlds/aquaria/docs/en_Aquaria.md b/worlds/aquaria/docs/en_Aquaria.md index c37f27568d..c3e5f54dd6 100644 --- a/worlds/aquaria/docs/en_Aquaria.md +++ b/worlds/aquaria/docs/en_Aquaria.md @@ -15,14 +15,14 @@ The locations in the randomizer are: - All Mithalas Urns - All Sunken City crates - Collectible treasure locations (including pet eggs and costumes) -- Beating Simon says +- Beating Simon Says - Li cave - Every Transportation Turtle (also called transturtle) - Locations where you get songs: - * Erulian spirit cristal + * Erulian spirit crystal * Energy status mini-boss * Beating Mithalan God boss - * Fish cave puzzle + * Fish Cave puzzle * Beating Drunian God boss * Beating Sun God boss * Breaking Li cage in the body @@ -61,4 +61,4 @@ what has been collected and who will receive it. ## When the player receives an item, what happens? When you receive an item, a message will pop up to inform you where you received -the item from and which one it was. \ No newline at end of file +the item from and which one it was. diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index ba42ac6d2c..198ccb0f62 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -10,148 +10,148 @@ from test.bases import WorldTestBase # Every location accessible after the home water. after_home_water_locations = [ "Sun Crystal", - "Home water, Transturtle", - "Open water top left area, bulb under the rock in the right path", - "Open water top left area, bulb under the rock in the left path", - "Open water top left area, bulb to the right of the save cristal", - "Open water top right area, bulb in the small path before Mithalas", - "Open water top right area, bulb in the path from the left entrance", - "Open water top right area, bulb in the clearing close to the bottom exit", - "Open water top right area, bulb in the big clearing close to the save cristal", - "Open water top right area, bulb in the big clearing to the top exit", - "Open water top right area, first urn in the Mithalas exit", - "Open water top right area, second urn in the Mithalas exit", - "Open water top right area, third urn in the Mithalas exit", - "Open water top right area, bulb in the turtle room", - "Open water top right area, Transturtle", - "Open water bottom left area, bulb behind the chomper fish", - "Open water bottom left area, bulb inside the lowest fish pass", - "Open water skeleton path, bulb close to the right exit", - "Open water skeleton path, bulb behind the chomper fish", - "Open water skeleton path, King skull", + "Home Water, Transturtle", + "Open Water top left area, bulb under the rock in the right path", + "Open Water top left area, bulb under the rock in the left path", + "Open Water top left area, bulb to the right of the save crystal", + "Open Water top right area, bulb in the small path before Mithalas", + "Open Water top right area, bulb in the path from the left entrance", + "Open Water top right area, bulb in the clearing close to the bottom exit", + "Open Water top right area, bulb in the big clearing close to the save crystal", + "Open Water top right area, bulb in the big clearing to the top exit", + "Open Water top right area, first urn in the Mithalas exit", + "Open Water top right area, second urn in the Mithalas exit", + "Open Water top right area, third urn in the Mithalas exit", + "Open Water top right area, bulb in the turtle room", + "Open Water top right area, Transturtle", + "Open Water bottom left area, bulb behind the chomper fish", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Open Water skeleton path, bulb close to the right exit", + "Open Water skeleton path, bulb behind the chomper fish", + "Open Water skeleton path, King Skull", "Arnassi Ruins, bulb in the right part", "Arnassi Ruins, bulb in the left part", "Arnassi Ruins, bulb in the center part", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Arnassi ruins, Arnassi Armor", - "Arnassi Ruins, Arnassi statue", + "Arnassi Ruins, Song Plant Spore", + "Arnassi Ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Statue", "Arnassi Ruins, Transturtle", - "Arnassi ruins, Crab armor", - "Simon says area, Transturtle", - "Mithalas city, first bulb in the left city part", - "Mithalas city, second bulb in the left city part", - "Mithalas city, bulb in the right part", - "Mithalas city, bulb at the top of the city", - "Mithalas city, first bulb in a broken home", - "Mithalas city, second bulb in a broken home", - "Mithalas city, bulb in the bottom left part", - "Mithalas city, first bulb in one of the homes", - "Mithalas city, second bulb in one of the homes", - "Mithalas city, first urn in one of the homes", - "Mithalas city, second urn in one of the homes", - "Mithalas city, first urn in the city reserve", - "Mithalas city, second urn in the city reserve", - "Mithalas city, third urn in the city reserve", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", - "Mithalas city castle, bulb in the flesh hole", - "Mithalas city castle, Blue banner", - "Mithalas city castle, urn in the bedroom", - "Mithalas city castle, first urn of the single lamp path", - "Mithalas city castle, second urn of the single lamp path", - "Mithalas city castle, urn in the bottom room", - "Mithalas city castle, first urn on the entrance path", - "Mithalas city castle, second urn on the entrance path", - "Mithalas castle, beating the priests", - "Mithalas city castle, Trident head", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", - "Cathedral underground, bulb in the center part", - "Cathedral underground, first bulb in the top left part", - "Cathedral underground, second bulb in the top left part", - "Cathedral underground, third bulb in the top left part", - "Cathedral underground, bulb close to the save cristal", - "Cathedral underground, bulb in the bottom right path", + "Arnassi Ruins, Crab Armor", + "Simon Says area, Transturtle", + "Mithalas City, first bulb in the left city part", + "Mithalas City, second bulb in the left city part", + "Mithalas City, bulb in the right part", + "Mithalas City, bulb at the top of the city", + "Mithalas City, first bulb in a broken home", + "Mithalas City, second bulb in a broken home", + "Mithalas City, bulb in the bottom left part", + "Mithalas City, first bulb in one of the homes", + "Mithalas City, second bulb in one of the homes", + "Mithalas City, first urn in one of the homes", + "Mithalas City, second urn in one of the homes", + "Mithalas City, first urn in the city reserve", + "Mithalas City, second urn in the city reserve", + "Mithalas City, third urn in the city reserve", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", + "Mithalas City Castle, bulb in the flesh hole", + "Mithalas City Castle, Blue banner", + "Mithalas City Castle, urn in the bedroom", + "Mithalas City Castle, first urn of the single lamp path", + "Mithalas City Castle, second urn of the single lamp path", + "Mithalas City Castle, urn in the bottom room", + "Mithalas City Castle, first urn on the entrance path", + "Mithalas City Castle, second urn on the entrance path", + "Mithalas City Castle, beating the Priests", + "Mithalas City Castle, Trident Head", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", + "Cathedral Underground, bulb in the center part", + "Cathedral Underground, first bulb in the top left part", + "Cathedral Underground, second bulb in the top left part", + "Cathedral Underground, third bulb in the top left part", + "Cathedral Underground, bulb close to the save crystal", + "Cathedral Underground, bulb in the bottom right path", "Cathedral boss area, beating Mithalan God", "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", "Kelp Forest top left, Jelly Egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Kelp Forest top right area, bulb under the rock in the right path", "Kelp Forest top right area, bulb at the left of the center clearing", "Kelp Forest top right area, bulb in the left path's big room", "Kelp Forest top right area, bulb in the left path's small room", "Kelp Forest top right area, bulb at the top of the center clearing", - "Kelp forest top right area, Black pearl", + "Kelp Forest top right area, Black Pearl", "Kelp Forest top right area, bulb in the top fish pass", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker baby", "Kelp Forest bottom left area, Transturtle", - "Kelp forest bottom right area, Odd Container", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest boss area, beating Drunian God", "Kelp Forest boss room, bulb at the bottom of the area", - "Kelp Forest bottom left area, Fish cave puzzle", + "Kelp Forest bottom left area, Fish Cave puzzle", "Kelp Forest sprite cave, bulb inside the fish pass", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", - "The veil top left area, In the Li cave", - "The veil top left area, bulb under the rock in the top right path", - "The veil top left area, bulb hidden behind the blocking rock", - "The veil top left area, Transturtle", - "The veil top left area, bulb inside the fish pass", + "The Veil top left area, In Li's cave", + "The Veil top left area, bulb under the rock in the top right path", + "The Veil top left area, bulb hidden behind the blocking rock", + "The Veil top left area, Transturtle", + "The Veil top left area, bulb inside the fish pass", "Turtle cave, Turtle Egg", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", - "The veil top right area, bulb in the middle of the wall jump cliff", - "The veil top right area, golden starfish at the bottom right of the bottom path", - "The veil top right area, bulb in the top of the water fall", - "The veil top right area, Transturtle", - "The veil bottom area, bulb in the left path", - "The veil bottom area, bulb in the spirit path", - "The veil bottom area, Verse egg", - "The veil bottom area, Stone Head", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sun temple, bulb in the top left part", - "Sun temple, bulb in the top right part", - "Sun temple, bulb at the top of the high dark room", - "Sun temple, Golden Gear", - "Sun temple, first bulb of the temple", - "Sun temple, bulb on the left part", - "Sun temple, bulb in the hidden room of the right part", - "Sun temple, Sun key", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", + "The Veil top right area, bulb in the middle of the wall jump cliff", + "The Veil top right area, Golden Starfish", + "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, Transturtle", + "The Veil bottom area, bulb in the left path", + "The Veil bottom area, bulb in the spirit path", + "The Veil bottom area, Verse Egg", + "The Veil bottom area, Stone Head", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sun Temple, bulb in the top left part", + "Sun Temple, bulb in the top right part", + "Sun Temple, bulb at the top of the high dark room", + "Sun Temple, Golden Gear", + "Sun Temple, first bulb of the temple", + "Sun Temple, bulb on the left part", + "Sun Temple, bulb in the hidden room of the right part", + "Sun Temple, Sun Key", "Sun Worm path, first path bulb", "Sun Worm path, second path bulb", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "Sun temple boss area, beating Sun God", + "Sun Temple boss area, beating Sun God", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -159,40 +159,40 @@ after_home_water_locations = [ "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "The whale, Verse egg", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, first bulb in the turtle room", - "Final boss area, second bulbs in the turtle room", - "Final boss area, third bulbs in the turtle room", - "Final boss area, Transturtle", - "Final boss area, bulb in the boss third form room", - "Kelp forest, beating Simon says", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "The Whale, Verse Egg", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, first bulb in the turtle room", + "Final Boss area, second bulb in the turtle room", + "Final Boss area, third bulb in the turtle room", + "Final Boss area, Transturtle", + "Final Boss area, bulb in the boss third form room", + "Simon Says area, beating Simon Says", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index a8d5551586..c25070d470 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -13,33 +13,33 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mithalas castle, beating the priests", - "Arnassi ruins, Crab armor", - "Arnassi ruins, Song plant spore on the top of the ruins", - "Mithalas city, first bulb at the end of the top path", - "Mithalas city, second bulb at the end of the top path", - "Mithalas city, bulb in the top path", - "Mithalas city, Mithalas pot", - "Mithalas city, urn in the cathedral flower tube entrance", + "Mithalas City Castle, beating the Priests", + "Arnassi Ruins, Crab Armor", + "Arnassi Ruins, Song Plant Spore", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Cathedral flower tube entrance", "Mermog cave, Piranha Egg", - "Mithalas cathedral, Mithalan Dress", - "Turtle cave, bulb in bubble cliff", - "Turtle cave, Urchin costume", + "Mithalas Cathedral, Mithalan Dress", + "Turtle cave, bulb in Bubble Cliff", + "Turtle cave, Urchin Costume", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Octopus cave, Dumbo Egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City, bulb on top of the boss area", + "Octopus Cave, Dumbo Egg", "Beating the Golem", "Beating Mergog", "Beating Crabbius Maximus", "Beating Octopus Prime", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, Jellyfish Costume", - "King Jellyfish cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", "Beating King Jellyfish God Prime", "Beating Mithalan priests", "Sunken City cleared" diff --git a/worlds/aquaria/test/test_bind_song_access.py b/worlds/aquaria/test/test_bind_song_access.py index b137d48ca9..ca663369cc 100644 --- a/worlds/aquaria/test/test_bind_song_access.py +++ b/worlds/aquaria/test/test_bind_song_access.py @@ -17,19 +17,19 @@ class BindSongAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song""" locations = [ - "Verse cave right area, Big Seed", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_bind_song_option_access.py b/worlds/aquaria/test/test_bind_song_option_access.py index 522a064b62..a75ef60cdf 100644 --- a/worlds/aquaria/test/test_bind_song_option_access.py +++ b/worlds/aquaria/test/test_bind_song_option_access.py @@ -18,24 +18,24 @@ class BindSongOptionAccessTest(AquariaTestBase): def test_bind_song_location(self) -> None: """Test locations that require Bind song with the bind song needed option activated""" locations = [ - "Verse cave right area, Big Seed", - "Verse cave left area, bulb under the rock at the end of the path", - "Home water, bulb under the rock in the left path from the verse cave", - "Song cave, bulb under the rock close to the song door", - "Song cave, bulb under the rock in the path to the singing statues", - "Naija's home, bulb under the rock at the right of the main path", - "Home water, bulb in the path below Nautilus Prime", - "Home water, bulb in the bottom left room", - "Home water, Nautilus Egg", - "Song cave, Verse egg", - "Energy temple first area, beating the energy statue", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple first area, Energy Idol", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", + "Verse Cave right area, Big Seed", + "Verse Cave left area, bulb under the rock at the end of the path", + "Home Water, bulb under the rock in the left path from the Verse Cave", + "Song Cave, bulb under the rock close to the song door", + "Song Cave, bulb under the rock in the path to the singing statues", + "Naija's Home, bulb under the rock at the right of the main path", + "Home Water, bulb in the path below Nautilus Prime", + "Home Water, bulb in the bottom left room", + "Home Water, Nautilus Egg", + "Song Cave, Verse Egg", + "Energy Temple first area, beating the Energy Statue", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple first area, Energy Idol", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", *after_home_water_locations ] items = [["Bind song"]] diff --git a/worlds/aquaria/test/test_confined_home_water.py b/worlds/aquaria/test/test_confined_home_water.py index f4e0e7b679..72fddfb404 100644 --- a/worlds/aquaria/test/test_confined_home_water.py +++ b/worlds/aquaria/test/test_confined_home_water.py @@ -16,5 +16,5 @@ class ConfinedHomeWaterAccessTest(AquariaTestBase): def test_confine_home_water_location(self) -> None: """Test region accessible with confined home water""" - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_dual_song_access.py b/worlds/aquaria/test/test_dual_song_access.py index 14c921d7cf..8266ffb181 100644 --- a/worlds/aquaria/test/test_dual_song_access.py +++ b/worlds/aquaria/test/test_dual_song_access.py @@ -16,10 +16,10 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require the dual song""" locations = [ - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Dual form"]] diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index edfe8a3f6c..ce4ed40994 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -17,41 +17,41 @@ class EnergyFormAccessTest(AquariaTestBase): def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Home water, Nautilus Egg", - "Naija's home, bulb after the energy door", - "Energy temple first area, bulb in the bottom room blocked by a rock", - "Energy temple second area, bulb under the rock", - "Energy temple bottom entrance, Krotite armor", - "Energy temple third area, bulb in the bottom path", - "Energy temple boss area, Fallen god tooth", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", - "Mithalas cathedral, first urn in the top right room", - "Mithalas cathedral, second urn in the top right room", - "Mithalas cathedral, third urn in the top right room", - "Mithalas cathedral, urn in the flesh room with fleas", - "Mithalas cathedral, first urn in the bottom right path", - "Mithalas cathedral, second urn in the bottom right path", - "Mithalas cathedral, urn behind the flesh vein", - "Mithalas cathedral, urn in the top left eyes boss room", - "Mithalas cathedral, first urn in the path behind the flesh vein", - "Mithalas cathedral, second urn in the path behind the flesh vein", - "Mithalas cathedral, third urn in the path behind the flesh vein", - "Mithalas cathedral, one of the urns in the top right room", - "Mithalas cathedral, Mithalan Dress", - "Mithalas cathedral right area, urn below the left entrance", + "Home Water, Nautilus Egg", + "Naija's Home, bulb after the energy door", + "Energy Temple first area, bulb in the bottom room blocked by a rock", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple boss area, Fallen God Tooth", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "Mithalas Cathedral, third urn in the path behind the flesh vein", + "Mithalas Cathedral, fourth urn in the top right room", + "Mithalas Cathedral, Mithalan Dress", + "Mithalas Cathedral right area, urn below the left entrance", "Cathedral boss area, beating Mithalan God", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", - "Kelp forest boss area, beating Drunian God", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest boss area, beating Drunian God", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "Sun temple boss area, beating Sun God", - "Arnassi ruins, Crab armor", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "Sun Temple boss area, beating Sun God", + "Arnassi Ruins, Crab Armor", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Sunken City, bulb on top of the boss area", + "Final Boss area, bulb in the boss third form room", "Beating Fallen God", "Beating Mithalan God", "Beating Drunian God", @@ -69,4 +69,4 @@ class EnergyFormAccessTest(AquariaTestBase): "Objective complete", ] items = [["Energy form"]] - self.assertAccessDependency(locations, items) \ No newline at end of file + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index 3077237172..d252bb1f18 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -16,22 +16,22 @@ class FishFormAccessTest(AquariaTestBase): def test_fish_form_location(self) -> None: """Test locations that require fish form""" locations = [ - "The veil top left area, bulb inside the fish pass", - "Mithalas city, Doll", - "Mithalas city, urn inside a home fish pass", + "The Veil top left area, bulb inside the fish pass", + "Mithalas City, Doll", + "Mithalas City, urn inside a home fish pass", "Kelp Forest top right area, bulb in the top fish pass", - "The veil bottom area, Verse egg", - "Open water bottom left area, bulb inside the lowest fish pass", - "Kelp Forest top left area, bulb close to the Verse egg", - "Kelp forest top left area, Verse egg", + "The Veil bottom area, Verse Egg", + "Open Water bottom left area, bulb inside the lowest fish pass", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Kelp Forest top left area, Verse Egg", "Mermog cave, bulb in the left part of the cave", "Mermog cave, Piranha Egg", "Beating Mergog", - "Octopus cave, Dumbo Egg", - "Octopus cave, bulb in the path below the octopus cave path", + "Octopus Cave, Dumbo Egg", + "Octopus Cave, bulb in the path below the Octopus Cave path", "Beating Octopus Prime", "Abyss left area, bulb in the bottom fish pass", - "Arnassi ruins, Arnassi Armor" + "Arnassi Ruins, Arnassi Armor" ] items = [["Fish form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index e26d5b5fcd..42adc90e5a 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -16,27 +16,27 @@ class LiAccessTest(AquariaTestBase): def test_li_song_location(self) -> None: """Test locations that require Li""" locations = [ - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Beating the Golem", "Sunken City cleared", "Objective complete" diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index 49414e5ace..41e65cb30d 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -20,19 +20,19 @@ class LightAccessTest(AquariaTestBase): # Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be # tested. # "Third secret", - # "Sun temple, bulb in the top left part", - # "Sun temple, bulb in the top right part", - # "Sun temple, bulb at the top of the high dark room", - # "Sun temple, Golden Gear", + # "Sun Temple, bulb in the top left part", + # "Sun Temple, bulb in the top right part", + # "Sun Temple, bulb at the top of the high dark room", + # "Sun Temple, Golden Gear", # "Sun Worm path, first path bulb", # "Sun Worm path, second path bulb", # "Sun Worm path, first cliff bulb", - "Octopus cave, Dumbo Egg", - "Kelp forest bottom right area, Odd Container", - "Kelp forest top right area, Black pearl", + "Octopus Cave, Dumbo Egg", + "Kelp Forest bottom right area, Odd Container", + "Kelp Forest top right area, Black Pearl", "Abyss left area, bulb in hidden path room", "Abyss left area, bulb in the right part", - "Abyss left area, Glowing seed", + "Abyss left area, Glowing Seed", "Abyss left area, Glowing Plant", "Abyss left area, bulb in the bottom fish pass", "Abyss right area, bulb behind the rock in the whale room", @@ -40,32 +40,32 @@ class LightAccessTest(AquariaTestBase): "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", "Abyss right area, Transturtle", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", "Beating King Jellyfish God Prime", - "The whale, Verse egg", + "The Whale, Verse Egg", "First secret", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Sunken City cleared", "Beating the Golem", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete", ] - items = [["Sun form", "Baby dumbo", "Has sun crystal"]] + items = [["Sun form", "Baby Dumbo", "Has sun crystal"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 89e7ceecbb..b380e5048f 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -16,41 +16,41 @@ class NatureFormAccessTest(AquariaTestBase): def test_nature_form_location(self) -> None: """Test locations that require nature form""" locations = [ - "Song cave, Anemone seed", - "Energy temple blaster room, Blaster egg", + "Song Cave, Anemone Seed", + "Energy Temple blaster room, Blaster Egg", "Beating Blaster Peg Prime", - "Kelp forest top left area, Verse egg", - "Kelp Forest top left area, bulb close to the Verse egg", - "Mithalas castle, beating the priests", + "Kelp Forest top left area, Verse Egg", + "Kelp Forest top left area, bulb close to the Verse Egg", + "Mithalas City Castle, beating the Priests", "Kelp Forest sprite cave, bulb in the second room", - "Kelp Forest Sprite Cave, Seed bag", + "Kelp Forest sprite cave, Seed Bag", "Beating Mithalan priests", "Abyss left area, bulb in the bottom fish pass", - "Bubble cave, Verse egg", + "Bubble Cave, Verse Egg", "Beating Mantis Shrimp Prime", - "Sunken city right area, crate close to the save cristal", - "Sunken city right area, crate in the left bottom room", - "Sunken city left area, crate in the little pipe room", - "Sunken city left area, crate close to the save cristal", - "Sunken city left area, crate before the bedroom", - "Sunken city left area, Girl Costume", - "Sunken city, bulb on the top of the boss area (boiler room)", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "Sunken City left area, Girl Costume", + "Sunken City, bulb on top of the boss area", "Beating the Golem", "Sunken City cleared", - "The body center area, breaking li cage", - "The body main area, bulb on the main path blocking tube", - "The body left area, first bulb in the top face room", - "The body left area, second bulb in the top face room", - "The body left area, bulb below the water stream", - "The body left area, bulb in the top path to the top face room", - "The body left area, bulb in the bottom face room", - "The body right area, bulb in the top face room", - "The body right area, bulb in the top path to the bottom face room", - "The body right area, bulb in the bottom face room", - "The body bottom area, bulb in the Jelly Zap room", - "The body bottom area, bulb in the nautilus room", - "The body bottom area, Mutant Costume", - "Final boss area, bulb in the boss third form room", + "The Body center area, breaking Li's cage", + "The Body main area, bulb on the main path blocking tube", + "The Body left area, first bulb in the top face room", + "The Body left area, second bulb in the top face room", + "The Body left area, bulb below the water stream", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Nature form"]] diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 5876ff31aa..817b9547a8 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 6450236097..2b7c8ddac9 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -15,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): } unfillable_locations = [ - "Energy temple boss area, Fallen god tooth", + "Energy Temple boss area, Fallen God Tooth", "Cathedral boss area, beating Mithalan God", - "Kelp forest boss area, beating Drunian God", - "Sun temple boss area, beating Sun God", - "Sunken city, bulb on the top of the boss area (boiler room)", - "Home water, Nautilus Egg", - "Energy temple blaster room, Blaster egg", - "Mithalas castle, beating the priests", + "Kelp Forest boss area, beating Drunian God", + "Sun Temple boss area, beating Sun God", + "Sunken City, bulb on top of the boss area", + "Home Water, Nautilus Egg", + "Energy Temple blaster room, Blaster Egg", + "Mithalas City Castle, beating the Priests", "Mermog cave, Piranha Egg", - "Octopus cave, Dumbo Egg", - "King Jellyfish cave, bulb in the right path from King Jelly", - "King Jellyfish cave, Jellyfish Costume", - "Final boss area, bulb in the boss third form room", + "Octopus Cave, Dumbo Egg", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The veil top right area, bulb in the top of the water fall", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", + "The Veil top right area, bulb in the top of the waterfall", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp forest bottom left area, Walker baby", - "Sun temple, Sun key", - "The body bottom area, Mutant Costume", - "Sun temple, bulb in the hidden room of the right part", - "Arnassi ruins, Arnassi Armor", + "Kelp Forest bottom left area, Walker baby", + "Sun Temple, Sun Key", + "The Body bottom area, Mutant Costume", + "Sun Temple, bulb in the hidden room of the right part", + "Arnassi Ruins, Arnassi Armor", ] def test_unconfine_home_water_both_location_fillable(self) -> None: diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 4d59d90a40..a6eec0da5d 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -13,24 +13,24 @@ class SpiritFormAccessTest(AquariaTestBase): def test_spirit_form_location(self) -> None: """Test locations that require spirit form""" locations = [ - "The veil bottom area, bulb in the spirit path", - "Mithalas city castle, Trident head", - "Open water skeleton path, King skull", - "Kelp forest bottom left area, Walker baby", + "The Veil bottom area, bulb in the spirit path", + "Mithalas City Castle, Trident Head", + "Open Water skeleton path, King Skull", + "Kelp Forest bottom left area, Walker baby", "Abyss right area, bulb behind the rock in the whale room", - "The whale, Verse egg", - "Ice cave, bulb in the room to the right", - "Ice cave, First bulbs in the top exit room", - "Ice cave, Second bulbs in the top exit room", - "Ice cave, third bulbs in the top exit room", - "Ice cave, bulb in the left room", - "Bubble cave, bulb in the left cave wall", - "Bubble cave, bulb in the right cave wall (behind the ice cristal)", - "Bubble cave, Verse egg", - "Sunken city left area, Girl Costume", + "The Whale, Verse Egg", + "Ice Cave, bulb in the room to the right", + "Ice Cave, first bulb in the top exit room", + "Ice Cave, second bulb in the top exit room", + "Ice Cave, third bulb in the top exit room", + "Ice Cave, bulb in the left room", + "Bubble Cave, bulb in the left cave wall", + "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", + "Bubble Cave, Verse Egg", + "Sunken City left area, Girl Costume", "Beating Mantis Shrimp Prime", "First secret", - "Arnassi ruins, Arnassi Armor", + "Arnassi Ruins, Arnassi Armor", ] items = [["Spirit form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index 159ab717c2..dfd732ec91 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -14,11 +14,11 @@ class SunFormAccessTest(AquariaTestBase): """Test locations that require sun form""" locations = [ "First secret", - "The whale, Verse egg", + "The Whale, Verse Egg", "Abyss right area, bulb behind the rock in the whale room", - "Octopus cave, Dumbo Egg", + "Octopus Cave, Dumbo Egg", "Beating Octopus Prime", - "Final boss area, bulb in the boss third form room", + "Final Boss area, bulb in the boss third form room", "Objective complete" ] items = [["Sun form"]] diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_both.py b/worlds/aquaria/test/test_unconfine_home_water_via_both.py index 3af17f1b75..24d3adad97 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_both.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_both.py @@ -17,5 +17,5 @@ class UnconfineHomeWaterBothAccessTest(AquariaTestBase): def test_unconfine_home_water_both_location(self) -> None: """Test locations accessible with unconfined home water via energy door and transportation turtle""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py index bfa82d65ea..92eb8d0291 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_energy_door.py @@ -16,5 +16,5 @@ class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase): def test_unconfine_home_water_energy_door_location(self) -> None: """Test locations accessible with unconfined home water via energy door""" - self.assertTrue(self.can_reach_region("Open water top left area"), "Cannot reach Open water top left area") - self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") \ No newline at end of file + self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area") + self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room") diff --git a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py index 627a92db29..66c40d23f1 100644 --- a/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py +++ b/worlds/aquaria/test/test_unconfine_home_water_via_transturtle.py @@ -17,4 +17,4 @@ class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase): def test_unconfine_home_water_transturtle_location(self) -> None: """Test locations accessible with unconfined home water via transportation turtle""" self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room") - self.assertFalse(self.can_reach_region("Open water top left area"), "Can reach Open water top left area") \ No newline at end of file + self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area") diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 195339c380..2c57bc7301 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -34,7 +34,6 @@ class Bk_SudokuWorld(World): """ game = "Sudoku" web = Bk_SudokuWebWorld() - data_version = 1 item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index 9abcd81b20..a46fb55b95 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -32,7 +32,6 @@ class BlasphemousWorld(World): game: str = "Blasphemous" web = BlasphemousWeb() - data_version = 2 item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)} location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)} diff --git a/worlds/bomb_rush_cyberfunk/Options.py b/worlds/bomb_rush_cyberfunk/Options.py index 87fc2ca99c..80831d0645 100644 --- a/worlds/bomb_rush_cyberfunk/Options.py +++ b/worlds/bomb_rush_cyberfunk/Options.py @@ -9,7 +9,9 @@ else: class Logic(Choice): - """Choose the logic used by the randomizer.""" + """ + Choose the logic used by the randomizer. + """ display_name = "Logic" option_glitchless = 0 option_glitched = 1 @@ -17,26 +19,38 @@ class Logic(Choice): class SkipIntro(DefaultOnToggle): - """Skips escaping the police station. - Graffiti spots tagged during the intro will not unlock items.""" + """ + Skips escaping the police station. + + Graffiti spots tagged during the intro will not unlock items. + """ display_name = "Skip Intro" class SkipDreams(Toggle): - """Skips the dream sequences at the end of each chapter. - This can be changed later in the options menu inside the Archipelago phone app.""" + """ + Skips the dream sequences at the end of each chapter. + + This can be changed later in the options menu inside the Archipelago phone app. + """ display_name = "Skip Dreams" class SkipHands(Toggle): - """Skips spraying the lion statue hands after the dream in Chapter 5.""" + """ + Skips spraying the lion statue hands after the dream in Chapter 5. + """ display_name = "Skip Statue Hands" class TotalRep(Range): - """Change the total amount of REP in your world. + """ + Change the total amount of REP in your world. + At least 960 REP is needed to finish the game. - Will be rounded to the nearest number divisible by 8.""" + + Will be rounded to the nearest number divisible by 8. + """ display_name = "Total REP" range_start = 1000 range_end = 2000 @@ -74,12 +88,16 @@ class TotalRep(Range): class EndingREP(Toggle): - """Changes the final boss to require 1000 REP instead of 960 REP to start.""" + """ + Changes the final boss to require 1000 REP instead of 960 REP to start. + """ display_name = "Extra REP Required" class StartStyle(Choice): - """Choose which movestyle to start with.""" + """ + Choose which movestyle to start with. + """ display_name = "Starting Movestyle" option_skateboard = 2 option_inline_skates = 3 @@ -88,17 +106,22 @@ class StartStyle(Choice): class LimitedGraffiti(Toggle): - """Each graffiti design can only be used a limited number of times before being removed from your inventory. - In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti, - uses will not be counted. - If enabled, doing graffiti is disabled during crew battles, to prevent softlocking.""" + """ + Each graffiti design can only be used a limited number of times before being removed from your inventory. + + In some cases, such as completing a dream, using graffiti to defeat enemies, or spraying over your own graffiti, uses will not be counted. + + If enabled, doing graffiti is disabled during crew battles, to prevent softlocking. + """ display_name = "Limited Graffiti" class SGraffiti(Choice): - """Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run - out, or combined, meaning that unlocking new characters will add 5 uses that any character can use. - Has no effect if Limited Graffiti is disabled.""" + """ + Choose if small graffiti should be separate, meaning that you will need to switch characters every time you run out, or combined, meaning that unlocking new characters will add 5 uses that any character can use. + + Has no effect if Limited Graffiti is disabled. + """ display_name = "Small Graffiti Uses" option_separate = 0 option_combined = 1 @@ -106,19 +129,27 @@ class SGraffiti(Choice): class JunkPhotos(Toggle): - """Skip taking pictures of Polo for items.""" + """ + Skip taking pictures of Polo for items. + """ display_name = "Skip Polo Photos" class DontSavePhotos(Toggle): - """Photos taken with the Camera app will not be saved. - This can be changed later in the options menu inside the Archipelago phone app.""" + """ + Photos taken with the Camera app will not be saved. + + This can be changed later in the options menu inside the Archipelago phone app. + """ display_name = "Don't Save Photos" class ScoreDifficulty(Choice): - """Alters the score required to win score challenges and crew battles. - This can be changed later in the options menu inside the Archipelago phone app.""" + """ + Alters the score required to win score challenges and crew battles. + + This can be changed later in the options menu inside the Archipelago phone app. + """ display_name = "Score Difficulty" option_normal = 0 option_medium = 1 @@ -129,10 +160,14 @@ class ScoreDifficulty(Choice): class DamageMultiplier(Range): - """Multiplies all damage received. + """ + Multiplies all damage received. + At 3x, most damage will OHKO the player, including falling into pits. At 6x, all damage will OHKO the player. - This can be changed later in the options menu inside the Archipelago phone app.""" + + This can be changed later in the options menu inside the Archipelago phone app. + """ display_name = "Damage Multiplier" range_start = 1 range_end = 6 @@ -140,8 +175,11 @@ class DamageMultiplier(Range): class BRCDeathLink(DeathLink): - """When you die, everyone dies. The reverse is also true. - This can be changed later in the options menu inside the Archipelago phone app.""" + """ + When you die, everyone dies. The reverse is also true. + + This can be changed later in the options menu inside the Archipelago phone app. + """ @dataclass diff --git a/worlds/bomb_rush_cyberfunk/Rules.py b/worlds/bomb_rush_cyberfunk/Rules.py index 6f31882cb1..f59a428570 100644 --- a/worlds/bomb_rush_cyberfunk/Rules.py +++ b/worlds/bomb_rush_cyberfunk/Rules.py @@ -5,17 +5,17 @@ from .Regions import Stages def graffitiM(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitim", player) * 7 >= spots if limit \ + return state.count_group_unique("graffitim", player) * 7 >= spots if limit \ else state.has_group("graffitim", player) def graffitiL(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitil", player) * 6 >= spots if limit \ + return state.count_group_unique("graffitil", player) * 6 >= spots if limit \ else state.has_group("graffitil", player) def graffitiXL(state: CollectionState, player: int, limit: bool, spots: int) -> bool: - return state.count_group_exclusive("graffitixl", player) * 4 >= spots if limit \ + return state.count_group_unique("graffitixl", player) * 4 >= spots if limit \ else state.has_group("graffitixl", player) @@ -469,7 +469,7 @@ def spots_s_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = 5 + (state.count_group_exclusive("characters", player) * 5) + sprayable: int = 5 + (state.count_group_unique("characters", player) * 5) if total <= sprayable: return total else: @@ -492,7 +492,7 @@ def spots_s_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = 5 + (state.count_group_exclusive("characters", player) * 5) + sprayable: int = 5 + (state.count_group_unique("characters", player) * 5) if total <= sprayable: return total else: @@ -537,7 +537,7 @@ def spots_m_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = state.count_group_exclusive("graffitim", player) * 7 + sprayable: int = state.count_group_unique("graffitim", player) * 7 if total <= sprayable: return total else: @@ -563,7 +563,7 @@ def spots_m_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = state.count_group_exclusive("graffitim", player) * 7 + sprayable: int = state.count_group_unique("graffitim", player) * 7 if total <= sprayable: return total else: @@ -614,7 +614,7 @@ def spots_l_glitchless(state: CollectionState, player: int, limit: bool, access_ break if limit: - sprayable: int = state.count_group_exclusive("graffitil", player) * 6 + sprayable: int = state.count_group_unique("graffitil", player) * 6 if total <= sprayable: return total else: @@ -641,7 +641,7 @@ def spots_l_glitched(state: CollectionState, player: int, limit: bool, access_ca break if limit: - sprayable: int = state.count_group_exclusive("graffitil", player) * 6 + sprayable: int = state.count_group_unique("graffitil", player) * 6 if total <= sprayable: return total else: @@ -685,7 +685,7 @@ def spots_xl_glitchless(state: CollectionState, player: int, limit: bool, access break if limit: - sprayable: int = state.count_group_exclusive("graffitixl", player) * 4 + sprayable: int = state.count_group_unique("graffitixl", player) * 4 if total <= sprayable: return total else: @@ -712,7 +712,7 @@ def spots_xl_glitched(state: CollectionState, player: int, limit: bool, access_c break if limit: - sprayable: int = state.count_group_exclusive("graffitixl", player) * 4 + sprayable: int = state.count_group_unique("graffitixl", player) * 4 if total <= sprayable: return total else: diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index d922c0277a..fe261dc94d 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -39,8 +39,6 @@ class BumpStikWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 1 - required_client_version = (0, 3, 8) options: BumpstikOptions diff --git a/worlds/celeste64/Options.py b/worlds/celeste64/Options.py index a2d142b8a5..9a67e7d7d4 100644 --- a/worlds/celeste64/Options.py +++ b/worlds/celeste64/Options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import Choice, Range, Toggle, DeathLink, PerGameCommonOptions +from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions class DeathLinkAmnesty(Range): @@ -47,7 +47,9 @@ class MoveShuffle(Toggle): - Air Dash - Skid Jump - Climb + NOTE: Having Move Shuffle and Standard Logic Difficulty will guarantee that one of the four Move items will be immediately accessible + WARNING: Combining Move Shuffle and Hard Logic Difficulty can require very difficult tricks """ display_name = "Move Shuffle" @@ -75,7 +77,9 @@ class Carsanity(Toggle): class BadelineChaserSource(Choice): """ What type of action causes more Badeline Chasers to start spawning + Locations: The number of locations you've checked contributes to Badeline Chasers + Strawberries: The number of Strawberry items you've received contributes to Badeline Chasers """ display_name = "Badeline Chaser Source" @@ -86,7 +90,9 @@ class BadelineChaserSource(Choice): class BadelineChaserFrequency(Range): """ How many of the `Badeline Chaser Source` actions must occur to make each Badeline Chaser start spawning + NOTE: Choosing `0` disables Badeline Chasers entirely + WARNING: Turning on Badeline Chasers alongside Move Shuffle could result in extremely difficult situations """ display_name = "Badeline Chaser Frequency" @@ -104,6 +110,24 @@ class BadelineChaserSpeed(Range): default = 3 +celeste_64_option_groups = [ + OptionGroup("Goal Options", [ + TotalStrawberries, + StrawberriesRequiredPercentage, + ]), + OptionGroup("Sanity Options", [ + Friendsanity, + Signsanity, + Carsanity, + ]), + OptionGroup("Badeline Chasers", [ + BadelineChaserSource, + BadelineChaserFrequency, + BadelineChaserSpeed, + ]), +] + + @dataclass class Celeste64Options(PerGameCommonOptions): death_link: DeathLink diff --git a/worlds/celeste64/__init__.py b/worlds/celeste64/__init__.py index d7e074623b..7786e38123 100644 --- a/worlds/celeste64/__init__.py +++ b/worlds/celeste64/__init__.py @@ -7,7 +7,7 @@ from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_tab from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\ sign_location_data_table, car_location_data_table, location_table from .Names import ItemName, LocationName -from .Options import Celeste64Options +from .Options import Celeste64Options, celeste_64_option_groups class Celeste64WebWorld(WebWorld): @@ -24,6 +24,8 @@ class Celeste64WebWorld(WebWorld): tutorials = [setup_en] + option_groups = celeste_64_option_groups + class Celeste64World(World): """Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer. diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index b70c65bb08..c8b9587f85 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -33,8 +33,6 @@ class ChecksFinderWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 4 - def _get_checksfinder_data(self): return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 30c0e47f81..b5cc74d94a 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -37,7 +37,6 @@ class CliqueWorld(World): """The greatest game of all time.""" game = "Clique" - data_version = 3 web = CliqueWebWorld() option_definitions = clique_options location_name_to_id = location_table diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 84bf03ff27..0d384acc8f 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -8,7 +8,7 @@ from BaseClasses import Item, Region, Tutorial, ItemClassification from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id from .entrances import verify_entrances, get_warp_entrances -from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle +from .options import CV64Options, cv64_option_groups, CharacterStages, DraculasCondition, SubWeaponShuffle from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \ shuffle_stages, generate_warps, get_region_names from .regions import get_region_info @@ -45,6 +45,8 @@ class CV64Web(WebWorld): ["Liquid Cat"] )] + option_groups = cv64_option_groups + class CV64World(World): """ @@ -62,7 +64,6 @@ class CV64World(World): options: CV64Options settings: typing.ClassVar[CV64Settings] topology_present = True - data_version = 1 item_name_to_id = get_item_names_to_ids() location_name_to_id = get_location_names_to_ids() diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index ff9c79f578..bea8ce3882 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -146,7 +146,7 @@ class Castlevania64Client(BizHawkClient): text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) - received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n" + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n" f"from {ctx.player_names[next_item.player]}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index da2b9f9496..93b417ad26 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -1,10 +1,11 @@ from dataclasses import dataclass -from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool +from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool class CharacterStages(Choice): - """Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end - of Villa and Castle Center.""" + """ + Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end of Villa and Castle Center. + """ display_name = "Character Stages" option_both = 0 option_branchless_both = 1 @@ -14,14 +15,18 @@ class CharacterStages(Choice): class StageShuffle(Toggle): - """Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character - stage slots if Character Stages is set to Both; they can only be somewhere on the main path. - Castle Keep will always be at the end of the line.""" + """ + Shuffles which stages appear in which stage slots. + Villa and Castle Center will never appear in any character stage slots if Character Stages is set to Both; they can only be somewhere on the main path. + Castle Keep will always be at the end of the line. + """ display_name = "Stage Shuffle" class StartingStage(Choice): - """Which stage to start at if Stage Shuffle is turned on.""" + """ + Which stage to start at if Stage Shuffle is turned on. + """ display_name = "Starting Stage" option_forest_of_silence = 0 option_castle_wall = 1 @@ -39,8 +44,9 @@ class StartingStage(Choice): class WarpOrder(Choice): - """Arranges the warps in the warp menu in whichever stage order chosen, - thereby changing the order they are unlocked in.""" + """ + Arranges the warps in the warp menu in whichever stage order chosen, thereby changing the order they are unlocked in. + """ display_name = "Warp Order" option_seed_stage_order = 0 option_vanilla_stage_order = 1 @@ -49,7 +55,9 @@ class WarpOrder(Choice): class SubWeaponShuffle(Choice): - """Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool.""" + """ + Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool. + """ display_name = "Sub-weapon Shuffle" option_off = 0 option_own_pool = 1 @@ -58,8 +66,10 @@ class SubWeaponShuffle(Choice): class SpareKeys(Choice): - """Puts an additional copy of every non-Special key item in the pool for every key item that there is. - Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them.""" + """ + Puts an additional copy of every non-Special key item in the pool for every key item that there is. + Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them. + """ display_name = "Spare Keys" option_off = 0 option_on = 1 @@ -68,14 +78,17 @@ class SpareKeys(Choice): class HardItemPool(Toggle): - """Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode - in the PAL version.""" + """ + Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode in the PAL version. + """ display_name = "Hard Item Pool" class Special1sPerWarp(Range): - """Sets how many Special1 jewels are needed per warp menu option unlock. - This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already.""" + """ + Sets how many Special1 jewels are needed per warp menu option unlock. + This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already. + """ range_start = 1 range_end = 10 default = 1 @@ -83,7 +96,9 @@ class Special1sPerWarp(Range): class TotalSpecial1s(Range): - """Sets how many Speical1 jewels are in the pool in total.""" + """ + Sets how many Speical1 jewels are in the pool in total. + """ range_start = 7 range_end = 70 default = 7 @@ -91,11 +106,13 @@ class TotalSpecial1s(Range): class DraculasCondition(Choice): - """Sets the requirement for unlocking and opening the door to Dracula's chamber. + """ + Sets the requirement for unlocking and opening the door to Dracula's chamber. None: No requirement. Door is unlocked from the start. Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated. Bosses: Kill a specified number of bosses with health bars and claim their Trophies. - Specials: Find a specified number of Special2 jewels shuffled in the main item pool.""" + Specials: Find a specified number of Special2 jewels shuffled in the main item pool. + """ display_name = "Dracula's Condition" option_none = 0 option_crystal = 1 @@ -105,7 +122,9 @@ class DraculasCondition(Choice): class PercentSpecial2sRequired(Range): - """Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s.""" + """ + Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s. + """ range_start = 1 range_end = 100 default = 80 @@ -113,7 +132,9 @@ class PercentSpecial2sRequired(Range): class TotalSpecial2s(Range): - """How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s.""" + """ + How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s. + """ range_start = 1 range_end = 70 default = 25 @@ -121,58 +142,70 @@ class TotalSpecial2s(Range): class BossesRequired(Range): - """How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses. - This will automatically adjust if there are fewer available bosses than the chosen number.""" + """ + How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses. + This will automatically adjust if there are fewer available bosses than the chosen number. + """ range_start = 1 range_end = 16 - default = 14 + default = 12 display_name = "Bosses Required" class CarrieLogic(Toggle): - """Adds the 2 checks inside Underground Waterway's crawlspace to the pool. + """ + Adds the 2 checks inside Underground Waterway's crawlspace to the pool. If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this. - Can be combined with Hard Logic to include Carrie-only tricks.""" + Can be combined with Hard Logic to include Carrie-only tricks. + """ display_name = "Carrie Logic" class HardLogic(Toggle): - """Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include - Carrie-only tricks. - See the Game Page for a full list of tricks and glitches that may be logically required.""" + """ + Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include Carrie-only tricks. + See the Game Page for a full list of tricks and glitches that may be logically required. + """ display_name = "Hard Logic" class MultiHitBreakables(Toggle): - """Adds the items that drop from the objects that break in three hits to the pool. There are 18 of these throughout - the game, adding up to 79 or 80 checks (depending on sub-weapons - being shuffled anywhere or not) in total with all stages. - The game will be modified to - remember exactly which of their items you've picked up instead of simply whether they were broken or not.""" + """ + Adds the items that drop from the objects that break in three hits to the pool. + There are 18 of these throughout the game, adding up to 79 or 80 checks (depending on sub-weapons being shuffled anywhere or not) in total with all stages. + The game will be modified to remember exactly which of their items you've picked up instead of simply whether they were broken or not. + """ display_name = "Multi-hit Breakables" class EmptyBreakables(Toggle): - """Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) - and some additional Red Jewels and/or moneybags into the item pool to compensate.""" + """ + Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) and some additional Red Jewels and/or moneybags into the item pool to compensate. + """ display_name = "Empty Breakables" class LizardLockerItems(Toggle): - """Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool. - Picking up all of these can be a very tedious luck-based process, so they are off by default.""" + """ + Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool. + Picking up all of these can be a very tedious luck-based process, so they are off by default. + """ display_name = "Lizard Locker Items" class Shopsanity(Toggle): - """Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will - revert to whatever it is in the vanilla game.""" + """ + Adds 7 one-time purchases from Renon's shop into the location pool. + After buying an item from a slot, it will revert to whatever it is in the vanilla game. + """ display_name = "Shopsanity" class ShopPrices(Choice): - """Randomizes the amount of gold each item costs in Renon's shop. - Use the below options to control how much or little an item can cost.""" + """ + Randomizes the amount of gold each item costs in Renon's shop. + Use the Minimum and Maximum Gold Price options to control how much or how little an item can cost. + """ display_name = "Shop Prices" option_vanilla = 0 option_randomized = 1 @@ -180,7 +213,9 @@ class ShopPrices(Choice): class MinimumGoldPrice(Range): - """The lowest amount of gold an item can cost in Renon's shop, divided by 100.""" + """ + The lowest amount of gold an item can cost in Renon's shop, divided by 100. + """ display_name = "Minimum Gold Price" range_start = 1 range_end = 50 @@ -188,7 +223,9 @@ class MinimumGoldPrice(Range): class MaximumGoldPrice(Range): - """The highest amount of gold an item can cost in Renon's shop, divided by 100.""" + """ + The highest amount of gold an item can cost in Renon's shop, divided by 100. + """ display_name = "Maximum Gold Price" range_start = 1 range_end = 50 @@ -196,8 +233,9 @@ class MaximumGoldPrice(Range): class PostBehemothBoss(Choice): - """Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating - Behemoth.""" + """ + Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating Behemoth. + """ display_name = "Post-Behemoth Boss" option_vanilla = 0 option_inverted = 1 @@ -207,7 +245,9 @@ class PostBehemothBoss(Choice): class RoomOfClocksBoss(Choice): - """Sets which boss is fought at Room of Clocks by which characters.""" + """ + Sets which boss is fought at Room of Clocks by which characters. + """ display_name = "Room of Clocks Boss" option_vanilla = 0 option_inverted = 1 @@ -217,7 +257,9 @@ class RoomOfClocksBoss(Choice): class RenonFightCondition(Choice): - """Sets the condition on which the Renon fight will trigger.""" + """ + Sets the condition on which the Renon fight will trigger. + """ display_name = "Renon Fight Condition" option_never = 0 option_spend_30k = 1 @@ -226,7 +268,9 @@ class RenonFightCondition(Choice): class VincentFightCondition(Choice): - """Sets the condition on which the vampire Vincent fight will trigger.""" + """ + Sets the condition on which the vampire Vincent fight will trigger. + """ display_name = "Vincent Fight Condition" option_never = 0 option_wait_16_days = 1 @@ -235,7 +279,9 @@ class VincentFightCondition(Choice): class BadEndingCondition(Choice): - """Sets the condition on which the currently-controlled character's Bad Ending will trigger.""" + """ + Sets the condition on which the currently-controlled character's Bad Ending will trigger. + """ display_name = "Bad Ending Condition" option_never = 0 option_kill_vincent = 1 @@ -244,24 +290,32 @@ class BadEndingCondition(Choice): class IncreaseItemLimit(DefaultOnToggle): - """Increases the holding limit of usable items from 10 to 99 of each item.""" + """ + Increases the holding limit of usable items from 10 to 99 of each item. + """ display_name = "Increase Item Limit" class NerfHealingItems(Toggle): - """Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%.""" + """ + Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%. + """ display_name = "Nerf Healing Items" class LoadingZoneHeals(DefaultOnToggle): - """Whether end-of-level loading zones restore health and cure status aliments or not. - Recommended off for those looking for more of a survival horror experience!""" + """ + Whether end-of-level loading zones restore health and cure status aliments or not. + Recommended off for those looking for more of a survival horror experience! + """ display_name = "Loading Zone Heals" class InvisibleItems(Choice): - """Sets which items are visible in their locations and which are invisible until picked up. - 'Chance' gives each item a 50/50 chance of being visible or invisible.""" + """ + Sets which items are visible in their locations and which are invisible until picked up. + 'Chance' gives each item a 50/50 chance of being visible or invisible. + """ display_name = "Invisible Items" option_vanilla = 0 option_reveal_all = 1 @@ -271,21 +325,25 @@ class InvisibleItems(Choice): class DropPreviousSubWeapon(Toggle): - """When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired.""" + """ + When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired. + """ display_name = "Drop Previous Sub-weapon" class PermanentPowerUps(Toggle): - """Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after - dying and/or continuing. - To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile.""" + """ + Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after dying and/or continuing. + To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile. + """ display_name = "Permanent PowerUps" class IceTrapPercentage(Range): - """Replaces a percentage of junk items with Ice Traps. - These will be visibly disguised as other items, and receiving one will freeze you - as if you were hit by Camilla's ice cloud attack.""" + """ + Replaces a percentage of junk items with Ice Traps. + These will be visibly disguised as other items, and receiving one will freeze you as if you were hit by Camilla's ice cloud attack. + """ display_name = "Ice Trap Percentage" range_start = 0 range_end = 100 @@ -293,7 +351,9 @@ class IceTrapPercentage(Range): class IceTrapAppearance(Choice): - """What items Ice Traps can possibly be disguised as.""" + """ + What items Ice Traps can possibly be disguised as. + """ display_name = "Ice Trap Appearance" option_major_only = 0 option_junk_only = 1 @@ -302,31 +362,34 @@ class IceTrapAppearance(Choice): class DisableTimeRestrictions(Toggle): - """Disables the restriction on every event and door that requires the current time - to be within a specific range, so they can be triggered at any time. + """ + Disables the restriction on every event and door that requires the current time to be within a specific range, so they can be triggered at any time. This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar. - The Villa coffin is not affected by this.""" + The Villa coffin is not affected by this. + """ display_name = "Disable Time Requirements" class SkipGondolas(Toggle): - """Makes jumping on and activating a gondola in Tunnel instantly teleport you - to the other station, thereby skipping the entire three-minute ride. - The item normally at the gondola transfer point is moved to instead be - near the red gondola at its station.""" + """ + Makes jumping on and activating a gondola in Tunnel instantly teleport you to the other station, thereby skipping the entire three-minute ride. + The item normally at the gondola transfer point is moved to instead be near the red gondola at its station. + """ display_name = "Skip Gondolas" class SkipWaterwayBlocks(Toggle): - """Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating - brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically - required if Shopsanity is on.""" + """ + Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating brick platforms won't have to be done. + Shopping at the Contract on the other side of them may still be logically required if Shopsanity is on. + """ display_name = "Skip Waterway Blocks" class Countdown(Choice): - """Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items - or the total check locations remaining in the stage you are currently in.""" + """ + Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items or the total check locations remaining in the stage you are currently in. + """ display_name = "Countdown" option_none = 0 option_majors = 1 @@ -335,19 +398,21 @@ class Countdown(Choice): class BigToss(Toggle): - """Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. + """ + Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. Press A while tossed to cancel the launch momentum and avoid being thrown off ledges. Hold Z to have all incoming damage be treated as it normally would. - Any tricks that might be possible with it are NOT considered in logic by any options.""" + Any tricks that might be possible with it are not in logic. + """ display_name = "Big Toss" class PantherDash(Choice): - """Hold C-right at any time to sprint way faster. Any tricks that might be - possible with it are NOT considered in logic by any options and any boss - fights with boss health meters, if started, are expected to be finished - before leaving their arenas if Dracula's Condition is bosses. Jumpless will - prevent jumping while moving at the increased speed to ensure logic cannot be broken with it.""" + """ + Hold C-right at any time to sprint way faster. + Any tricks that are possible with it are not in logic and any boss fights with boss health meters, if started, are expected to be finished before leaving their arenas if Dracula's Condition is bosses. + Jumpless will prevent jumping while moving at the increased speed to make logic harder to break with it. + """ display_name = "Panther Dash" option_off = 0 option_on = 1 @@ -356,19 +421,25 @@ class PantherDash(Choice): class IncreaseShimmySpeed(Toggle): - """Increases the speed at which characters shimmy left and right while hanging on ledges.""" + """ + Increases the speed at which characters shimmy left and right while hanging on ledges. + """ display_name = "Increase Shimmy Speed" class FallGuard(Toggle): - """Removes fall damage from landing too hard. Note that falling for too long will still result in instant death.""" + """ + Removes fall damage from landing too hard. Note that falling for too long will still result in instant death. + """ display_name = "Fall Guard" class BackgroundMusic(Choice): - """Randomizes or disables the music heard throughout the game. + """ + Randomizes or disables the music heard throughout the game. Randomized music is split into two pools: songs that loop and songs that don't. - The "lead-in" versions of some songs will be paired accordingly.""" + The "lead-in" versions of some songs will be paired accordingly. + """ display_name = "Background Music" option_normal = 0 option_disabled = 1 @@ -377,8 +448,10 @@ class BackgroundMusic(Choice): class MapLighting(Choice): - """Randomizes the lighting color RGB values on every map during every time of day to be literally anything. - The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects.""" + """ + Randomizes the lighting color RGB values on every map during every time of day to be literally anything. + The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects. + """ display_name = "Map Lighting" option_normal = 0 option_randomized = 1 @@ -386,12 +459,16 @@ class MapLighting(Choice): class CinematicExperience(Toggle): - """Enables an unused film reel effect on every cutscene in the game. Purely cosmetic.""" + """ + Enables an unused film reel effect on every cutscene in the game. Purely cosmetic. + """ display_name = "Cinematic Experience" class WindowColorR(Range): - """The red value for the background color of the text windows during gameplay.""" + """ + The red value for the background color of the text windows during gameplay. + """ display_name = "Window Color R" range_start = 0 range_end = 15 @@ -399,7 +476,9 @@ class WindowColorR(Range): class WindowColorG(Range): - """The green value for the background color of the text windows during gameplay.""" + """ + The green value for the background color of the text windows during gameplay. + """ display_name = "Window Color G" range_start = 0 range_end = 15 @@ -407,7 +486,9 @@ class WindowColorG(Range): class WindowColorB(Range): - """The blue value for the background color of the text windows during gameplay.""" + """ + The blue value for the background color of the text windows during gameplay. + """ display_name = "Window Color B" range_start = 0 range_end = 15 @@ -415,7 +496,9 @@ class WindowColorB(Range): class WindowColorA(Range): - """The alpha value for the background color of the text windows during gameplay.""" + """ + The alpha value for the background color of the text windows during gameplay. + """ display_name = "Window Color A" range_start = 0 range_end = 15 @@ -423,9 +506,10 @@ class WindowColorA(Range): class DeathLink(Choice): - """When you die, everyone dies. Of course the reverse is true too. - Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion - instead of the normal death animation.""" + """ + When you die, everyone dies. Of course the reverse is true too. + Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation. + """ display_name = "DeathLink" option_off = 0 alias_no = 0 @@ -437,6 +521,7 @@ class DeathLink(Choice): @dataclass class CV64Options(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool character_stages: CharacterStages stage_shuffle: StageShuffle starting_stage: StartingStage @@ -479,13 +564,26 @@ class CV64Options(PerGameCommonOptions): big_toss: BigToss panther_dash: PantherDash increase_shimmy_speed: IncreaseShimmySpeed - background_music: BackgroundMusic - map_lighting: MapLighting - fall_guard: FallGuard - cinematic_experience: CinematicExperience window_color_r: WindowColorR window_color_g: WindowColorG window_color_b: WindowColorB window_color_a: WindowColorA + background_music: BackgroundMusic + map_lighting: MapLighting + fall_guard: FallGuard + cinematic_experience: CinematicExperience death_link: DeathLink - start_inventory_from_pool: StartInventoryPool + + +cv64_option_groups = [ + OptionGroup("gameplay tweaks", [ + HardItemPool, ShopPrices, MinimumGoldPrice, MaximumGoldPrice, PostBehemothBoss, RoomOfClocksBoss, + RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems, + LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage, + IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash, + IncreaseShimmySpeed, FallGuard, DeathLink + ]), + OptionGroup("cosmetics", [ + WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience + ]) +] diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index a13235b12a..3dd5cb2d3c 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -1272,11 +1272,7 @@ _cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ ]] item_descriptions = { - "Cinders": """ - All four Cinders of a Lord. - - Once you have these four, you can fight Soul of Cinder and win the game. - """, + "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", } _all_items = _vanilla_items + _dlc_items diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b4c231cdea..0200109811 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -35,6 +35,8 @@ class DarkSouls3Web(WebWorld): tutorials = [setup_en, setup_fr] + item_descriptions = item_descriptions + class DarkSouls3World(World): """ @@ -47,7 +49,6 @@ class DarkSouls3World(World): option_definitions = dark_souls_options topology_present: bool = True web = DarkSouls3Web() - data_version = 8 base_id = 100000 enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) @@ -61,8 +62,6 @@ class DarkSouls3World(World): "Cinders of a Lord - Lothric Prince" } } - item_descriptions = item_descriptions - def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index efa199e1d0..8e4a1bf2a4 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -86,7 +86,7 @@ class DKC3SNIClient(SNIClient): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -99,9 +99,9 @@ class DKC3SNIClient(SNIClient): item = ctx.items_received[recv_index] recv_index += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index])) if item.item in item_rom_data: diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py index 06be30cf15..b114a503b9 100644 --- a/worlds/dkc3/Options.py +++ b/worlds/dkc3/Options.py @@ -1,13 +1,15 @@ from dataclasses import dataclass import typing -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList, PerGameCommonOptions +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions class Goal(Choice): """ Determines the goal of the seed + Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein + Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother """ display_name = "Goal" @@ -26,6 +28,7 @@ class IncludeTradeSequence(Toggle): class DKCoinsForGyrocopter(Range): """ How many DK Coins are needed to unlock the Gyrocopter + Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume that you will use this. @@ -93,6 +96,7 @@ class LevelShuffle(Toggle): class Difficulty(Choice): """ Which Difficulty Level to use + NORML: The Normal Difficulty HARDR: Many DK Barrels are removed TUFST: Most DK Barrels and all Midway Barrels are removed @@ -159,19 +163,40 @@ class StartingLifeCount(Range): default = 5 +dkc3_option_groups = [ + OptionGroup("Goal Options", [ + Goal, + KrematoaBonusCoinCost, + PercentageOfExtraBonusCoins, + NumberOfBananaBirds, + PercentageOfBananaBirds, + ]), + OptionGroup("Aesthetics", [ + Autosave, + MERRY, + MusicShuffle, + KongPaletteSwap, + StartingLifeCount, + ]), +] + + @dataclass class DKC3Options(PerGameCommonOptions): #death_link: DeathLink # Disabled - goal: Goal #include_trade_sequence: IncludeTradeSequence # Disabled - dk_coins_for_gyrocopter: DKCoinsForGyrocopter + + goal: Goal krematoa_bonus_coin_cost: KrematoaBonusCoinCost percentage_of_extra_bonus_coins: PercentageOfExtraBonusCoins number_of_banana_birds: NumberOfBananaBirds percentage_of_banana_birds: PercentageOfBananaBirds + + dk_coins_for_gyrocopter: DKCoinsForGyrocopter kongsanity: KONGsanity level_shuffle: LevelShuffle difficulty: Difficulty + autosave: Autosave merry: MERRY music_shuffle: MusicShuffle diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index b0e153dcd2..de6fb4a44a 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -4,20 +4,21 @@ import typing import math import threading -import settings from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from Options import PerGameCommonOptions -from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table -from .Locations import DKC3Location, all_locations, setup_locations -from .Options import DKC3Options -from .Regions import create_regions, connect_regions -from .Levels import level_list -from .Rules import set_rules -from .Names import ItemName, LocationName -from .Client import DKC3SNIClient -from worlds.AutoWorld import WebWorld, World -from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch import Patch +import settings +from worlds.AutoWorld import WebWorld, World + +from .Client import DKC3SNIClient +from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table +from .Levels import level_list +from .Locations import DKC3Location, all_locations, setup_locations +from .Names import ItemName, LocationName +from .Options import DKC3Options, dkc3_option_groups +from .Regions import create_regions, connect_regions +from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch +from .Rules import set_rules class DK3Settings(settings.Group): @@ -41,9 +42,11 @@ class DKC3Web(WebWorld): "setup/en", ["PoryGone"] ) - + tutorials = [setup_en] + option_groups = dkc3_option_groups + class DKC3World(World): """ @@ -58,7 +61,6 @@ class DKC3World(World): options: DKC3Options topology_present = False - data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index ca2862113f..a9dfcc5044 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -43,8 +43,6 @@ class DLCqworld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - data_version = 1 - options_dataclass = DLCQuestOptions options: DLCQuestOptions diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index ace33f994c..b6138ae071 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -42,7 +42,6 @@ class DOOM1993World(World): options: DOOM1993Options game = "DOOM 1993" web = DOOM1993Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index daad945535..38840f552a 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -43,7 +43,6 @@ class DOOM2World(World): options: DOOM2Options game = "DOOM II" web = DOOM2Web() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index d245e1bb7a..258a544532 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext): if ctx.locations_checked != research_data: bridge_logger.debug( f"New researches done: " - f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}") + f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) death_link_tick = data.get("death_link_tick", 0) @@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext): transfer_item: NetworkItem = ctx.items_received[ctx.send_index] item_id = transfer_item.item player_name = ctx.player_names[transfer_item.player] - item_name = ctx.item_names[item_id] + item_name = ctx.item_names.lookup_in_slot(item_id) factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}" ctx.send_index += 1 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 3b74757384..1ea2f6e4c9 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -95,7 +95,6 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 8 required_client_version = (0, 4, 2) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index ce5519b13a..3a50475068 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -40,7 +40,6 @@ class FF1World(World): settings_key = "ffr_options" game = "Final Fantasy" topology_present = False - data_version = 2 ff1_items = FF1Items() ff1_locations = FF1Locations() diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index b995cc427c..ac3e913709 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -56,8 +56,6 @@ class FFMQWorld(World): create_regions = create_regions set_rules = set_rules stage_set_rules = stage_set_rules - - data_version = 1 web = FFMQWebWorld() # settings: FFMQSettings @@ -216,4 +214,3 @@ class FFMQWorld(World): hint_data[self.player][location.address] += f"/{hint}" else: hint_data[self.player][location.address] = hint - diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 6b2ffdfee1..29f808b202 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -40,7 +40,6 @@ class GenericWorld(World): } hidden = True web = GenericWeb() - data_version = 1 def generate_early(self): self.multiworld.player_types[self.player] = SlotType.spectator # mark as spectator @@ -69,9 +68,3 @@ class PlandoItem(NamedTuple): raise exception(warning) else: self.warn(warning) - - -class PlandoConnection(NamedTuple): - entrance: str - exit: str - direction: str # entrance, exit or both diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 5b1b583e61..37467eeb46 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -79,7 +79,7 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan different weights. * `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this - is good for detailing the version of Archipelago this YAML was prepared for as, if it is rolled on an older version, + is good for detailing the version of Archipelago this YAML was prepared for. If it is rolled on an older version, options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it here to ensure it will be used is good practice. @@ -137,7 +137,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) locations. * `item_links` allows players to link their items into a group with the same item link name and game. The items declared in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links - can also have local and non local items, forcing the items to either be placed within the worlds of the group or in + can also have local and non-local items, forcing the items to either be placed within the worlds of the group or in worlds outside the group. If players have a varying amount of a specific item in the link, the lowest amount from the players will be the amount put into the group. @@ -277,7 +277,7 @@ one file, removing the need to manage separate files if one chooses to do so. As a precautionary measure, before submitting a multi-game yaml like this one in a synchronous/sync multiworld, please confirm that the other players in the multi are OK with what you are submitting, and please be fairly reasonable about -the submission. (ie. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered +the submission. (i.e. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered reasonable, but submitting a ChecksFinder alongside another game OR submitting multiple Slay the Spire runs is likely OK) @@ -295,7 +295,7 @@ requires: version: 0.3.2 Super Mario 64: progression_balancing: 50 - accessibilty: items + accessibility: items EnableCoinStars: false StrictCapRequirements: true StrictCannonRequirements: true @@ -315,7 +315,7 @@ name: Minecraft game: Minecraft Minecraft: progression_balancing: 50 - accessibilty: items + accessibility: items advancement_goal: 40 combat_difficulty: hard include_hard_advancements: false @@ -341,7 +341,7 @@ game: ChecksFinder ChecksFinder: progression_balancing: 50 - accessibilty: items + accessibility: items ``` The above example will generate 3 worlds - one Super Mario 64, one Minecraft, and one ChecksFinder. diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index 73cca66543..b751b8a3ec 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -123,10 +123,21 @@ again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". -Options that define a list, set, or dict can additionally have the character `+` added to the start of their name, which applies the contents of -the activated trigger to the already present equivalents in the game options. +## Adding or Removing from a List, Set, or Dict Option + +List, set, and dict options can additionally have values added to or removed from itself without overriding the existing +option value by prefixing the option name in the trigger block with `+` (add) or `-` (remove). The exact behavior for +each will depend on the option type. + +- For sets, `+` will add the value(s) to the set and `-` will remove the value(s) from the set. Sets do not allow + duplicates. +- For lists, `+` will add new values(s) to the list and `-` will remove the first matching values(s) it comes across. + Lists allow duplicate values. +- For dicts, `+` will add the value(s) to the given key(s) inside the dict if it exists, or add it otherwise. `-` is the + inverse operation of addition (and negative values are allowed). For example: + ```yaml Super Metroid: start_location: @@ -134,18 +145,18 @@ Super Metroid: aqueduct: 50 start_hints: - Morph Ball -triggers: - - option_category: Super Metroid - option_name: start_location - option_result: aqueduct - options: - Super Metroid: - +start_hints: - - Gravity Suit + start_inventory: + Power Bombs: 1 + triggers: + - option_category: Super Metroid + option_name: start_location + option_result: aqueduct + options: + Super Metroid: + +start_hints: + - Gravity Suit ``` -In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be created. -If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball. - -Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will -replace that value within the dict. +In this example, if the `start_location` option rolls `landing_site`, only a starting hint for Morph Ball will be +created. If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph +Ball. diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index c83cdb9477..fc5ffdd2de 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -41,7 +41,6 @@ class HereticWorld(World): options: HereticOptions game = "Heretic" web = HereticWeb() - data_version = 3 required_client_version = (0, 3, 9) item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} diff --git a/worlds/hk/GodhomeData.py b/worlds/hk/GodhomeData.py index 6e9d77f4dc..a2dd69ed73 100644 --- a/worlds/hk/GodhomeData.py +++ b/worlds/hk/GodhomeData.py @@ -9,7 +9,7 @@ def set_godhome_rules(hk_world, hk_set_rule): fn = partial(hk_set_rule, hk_world) required_events = { - "Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player), + "Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player) and state.has('Godtuner', player), "Defeated_Pantheon_5": lambda state: state.has('GG_Atrium_Roof', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and ((state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player) and state.has('COMBAT[Radiance]', player))), "GG_Atrium_Roof": lambda state: state.has('GG_Atrium', player) and state.has('Hit_Pantheon_5_Unlock_Orb', player) and state.has('LEFTCLAW', player), diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index f7b4420c74..f408528821 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -105,7 +105,7 @@ default_on = { "RandomizeVesselFragments", "RandomizeCharmNotches", "RandomizePaleOre", - "RandomizeRancidEggs" + "RandomizeRancidEggs", "RandomizeRelics", "RandomizeStags", "RandomizeLifebloodCocoons" diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1359bea5ce..fdaece8d34 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -154,7 +154,6 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} - data_version = 2 def __init__(self, world, player): super(HKWorld, self).__init__(world, player) @@ -659,6 +658,8 @@ class HKItem(Item): def __init__(self, name, advancement, code, type: str, player: int = None): if name == "Mimic_Grub": classification = ItemClassification.trap + elif name == "Godtuner": + classification = ItemClassification.progression elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"): classification = ItemClassification.progression_skip_balancing elif type == "Charm" and name not in progression_charms: diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py index 0c50fb42be..db9c316a7b 100644 --- a/worlds/hylics2/Options.py +++ b/worlds/hylics2/Options.py @@ -1,25 +1,38 @@ from dataclasses import dataclass from Options import Choice, Removed, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions + class PartyShuffle(Toggle): - """Shuffles party members into the pool. - Note that enabling this can potentially increase both the difficulty and length of a run.""" + """ + Shuffles party members into the item pool. + + Note that enabling this can significantly increase both the difficulty and length of a run. + """ display_name = "Shuffle Party Members" + class GestureShuffle(Choice): - """Choose where gestures will appear in the item pool.""" + """ + Choose where gestures will appear in the item pool. + """ display_name = "Shuffle Gestures" option_anywhere = 0 option_tvs_only = 1 option_default_locations = 2 default = 0 + class MedallionShuffle(Toggle): - """Shuffles red medallions into the pool.""" + """ + Shuffles red medallions into the item pool. + """ display_name = "Shuffle Red Medallions" + class StartLocation(Choice): - """Select the starting location from 1 of 4 positions.""" + """ + Select the starting location from 1 of 4 positions. + """ display_name = "Start Location" option_waynehouse = 0 option_viewaxs_edifice = 1 @@ -35,14 +48,23 @@ class StartLocation(Choice): return "TV Island" return super().get_option_name(value) + class ExtraLogic(DefaultOnToggle): - """Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.""" + """ + Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult. + """ display_name = "Extra Items in Logic" + class Hylics2DeathLink(DeathLink): - """When you die, everyone dies. The reverse is also true. + """ + When you die, everyone dies. The reverse is also true. + Note that this also includes death by using the PERISH gesture. - Can be toggled via in-game console command "/deathlink".""" + + Can be toggled via in-game console command "/deathlink". + """ + @dataclass class Hylics2Options(PerGameCommonOptions): diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index be7ebf1991..18bcb0edc1 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -37,8 +37,6 @@ class Hylics2World(World): options_dataclass = Hylics2Options options: Hylics2Options - data_version = 3 - def set_rules(self): Rules.set_rules(self) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index e33a680bc0..6faa8206c2 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -330,9 +330,9 @@ class KDL3SNIClient(SNIClient): item = ctx.items_received[recv_amount] recv_amount += 1 logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_amount, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) snes_buffered_write(ctx, KDL3_RECV_COUNT, pack("H", recv_amount)) item_idx = item.item & 0x00000F @@ -415,7 +415,7 @@ class KDL3SNIClient(SNIClient): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/Options.py index 336bd33bc5..e0a4f12f15 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/Options.py @@ -2,10 +2,14 @@ import random from dataclasses import dataclass from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions + PerGameCommonOptions, PlandoConnections from .Names import LocationName +class KDL3PlandoConnections(PlandoConnections): + entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + + class Goal(Choice): """ Zero: collect the Heart Stars, and defeat Zero in the Hyper Zone. @@ -400,6 +404,7 @@ class Gifting(Toggle): @dataclass class KDL3Options(PerGameCommonOptions): + plando_connections: KDL3PlandoConnections death_link: DeathLink game_language: GameLanguage goal: Goal diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ac27d8bbf5..407dcf9680 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -129,8 +129,8 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte } possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.multiworld.plando_connections[world.player]: - for connection in world.multiworld.plando_connections[world.player]: + if world.options.plando_connections: + for connection in world.options.plando_connections: try: entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) stage_world, stage_stage = connection.exit.rsplit(" ", 1) diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 11a17e63b7..4d3f4d70fa 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -2,7 +2,7 @@ import typing from argparse import Namespace from BaseClasses import MultiWorld, PlandoOptions, CollectionState -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -32,6 +32,5 @@ class KDL3TestBase(WorldTestBase): }) self.multiworld.set_options(args) self.multiworld.plando_options = PlandoOptions.connections - self.multiworld.plando_connections = self.options["plando_connections"] if "plando_connections" in self.options.keys() else [] for step in gen_steps: call_all(self.multiworld, step) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 433b4534d1..bde9abc409 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,5 +1,5 @@ from . import KDL3TestBase -from worlds.generic import PlandoConnection +from Options import PlandoConnection from ..Names import LocationName import typing @@ -49,12 +49,10 @@ class TestShiro(KDL3TestBase): options = { "open_world": False, "plando_connections": [ - [], - [ PlandoConnection("Grass Land 1", "Iceberg 5", "both"), PlandoConnection("Grass Land 2", "Ripple Field 5", "both"), PlandoConnection("Grass Land 3", "Grass Land 1", "both") - ]], + ], "stage_shuffle": "shuffled", "plando_options": "connections" } diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 6c7517f359..f7de0f41f9 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -78,11 +78,6 @@ class LinksAwakeningWorld(World): settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 1 - # ID of first item and location, could be hard-coded but code may be easier # to read with this as a propery. base_id = BASE_ID diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py index 93274f1d68..5309e85032 100644 --- a/worlds/landstalker/Hints.py +++ b/worlds/landstalker/Hints.py @@ -30,6 +30,9 @@ def generate_lithograph_hint(world: "LandstalkerWorld"): jewel_items = world.jewel_items for item in jewel_items: + if item.location is None: + continue + # Jewel hints are composed of 4 'words' shuffled randomly: # - the name of the player whose world contains said jewel (if not ours) # - the color of the jewel (if relevant) @@ -61,7 +64,7 @@ def generate_random_hints(world: "LandstalkerWorld"): excluded_items = ["Life Stock", "EkeEke"] progression_items = [item for item in multiworld.itempool if item.advancement and - item.name not in excluded_items] + item.name not in excluded_items and item.location is not None] local_own_progression_items = [item for item in progression_items if item.player == this_player and item.location.player == this_player] diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index 5e42fbecda..b0148269ea 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -1,8 +1,9 @@ from typing import Dict, Optional -from BaseClasses import Location +from BaseClasses import Location, ItemClassification, Item from .Regions import LandstalkerRegion from .data.item_source import ITEM_SOURCES_JSON +from .data.world_path import WORLD_PATHS_JSON BASE_LOCATION_ID = 4000 BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256 @@ -28,6 +29,18 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"]) region.locations.append(new_location) + # Create fake event locations that will be used to determine if some key regions has been visited + regions_with_entrance_checks = [] + for data in WORLD_PATHS_JSON: + if "requiredNodes" in data: + regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) + regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + for region_id in regions_with_entrance_checks: + region = regions_table[region_id] + location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") + location.place_locked_item(Item("event_visited_" + region_id, ItemClassification.progression, None, player)) + region.locations.append(location) + # Create a specific end location that will contain a fake win-condition item end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") regions_table["end"].locations.append(end_location) diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py index 21704194f1..27e5e2e993 100644 --- a/worlds/landstalker/Regions.py +++ b/worlds/landstalker/Regions.py @@ -37,7 +37,7 @@ def create_regions(world: "LandstalkerWorld"): for code, region_data in WORLD_NODES_JSON.items(): random_hint_name = None if "hints" in region_data: - random_hint_name = multiworld.random.choice(region_data["hints"]) + random_hint_name = world.random.choice(region_data["hints"]) region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name) regions_table[code] = region multiworld.regions.append(region) diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py index 51357c9480..94171944d7 100644 --- a/worlds/landstalker/Rules.py +++ b/worlds/landstalker/Rules.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): - return all([state.can_reach(region, None, player) for region in regions]) + return all(state.has("event_visited_" + region.code, player) for region in regions) def _landstalker_has_health(state: CollectionState, player: int, health): diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index baa1deb620..2b3dc41239 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -204,6 +204,9 @@ class LandstalkerWorld(World): for location in self.multiworld.get_locations(self.player): if location.parent_region.name in excluded_regions: location.progress_type = LocationProgressType.EXCLUDED + # We need to make that event non-progression since it would crash generation in reach_kazalt goal + if location.item is not None and location.item.name == "event_visited_king_nole_labyrinth_raft_entrance": + location.item.classification = ItemClassification.filler def get_starting_health(self): spawn_id = self.options.spawn_region.current_key diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 113c3928d2..302e7e1d85 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,12 +9,13 @@ from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions +from .options import LingoOptions, lingo_option_groups from .player_logic import LingoPlayerLogic from .regions import create_regions class LingoWebWorld(WebWorld): + option_groups = lingo_option_groups theme = "grass" tutorials = [Tutorial( "Multiworld Setup Guide", @@ -36,7 +37,6 @@ class LingoWorld(World): base_id = 444400 topology_present = True - data_version = 1 options_dataclass = LingoOptions options: LingoOptions diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index c33cad393b..4d6771a735 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -2052,6 +2052,7 @@ door: Rhyme Room Entrance Art Gallery: warp: True + Roof: True # by parkouring through the Bearer shortcut panels: RED: id: Color Arrow Room/Panel_red_afar @@ -2333,6 +2334,7 @@ # This is the MASTERY on the other side of THE FEARLESS. It can only be # accessed by jumping from the top of the tower. id: Master Room/Panel_mastery_mastery8 + location_name: The Fearless - MASTERY tag: midwhite hunt: True required_door: @@ -4098,6 +4100,7 @@ Number Hunt: room: Number Hunt door: Door to Directional Gallery + Roof: True # through ceiling of sunwarp panels: PEPPER: id: Backside Room/Panel_pepper_salt @@ -5390,6 +5393,7 @@ - The Artistic (Apple) - The Artistic (Lattice) check: True + location_name: The Artistic - Achievement achievement: The Artistic FINE: id: Ceiling Room/Panel_yellow_top_5 @@ -6046,7 +6050,7 @@ paintings: - id: symmetry_painting_a_5 orientation: east - - id: symmetry_painting_a_5 + - id: symmetry_painting_b_5 disable: True The Wondrous (Window): entrances: @@ -6814,9 +6818,6 @@ tag: syn rhyme subtag: bot link: rhyme FALL - LEAP: - id: Double Room/Panel_leap_leap - tag: midwhite doors: Exit: id: Double Room Area Doors/Door_room_exit @@ -7065,6 +7066,9 @@ tag: syn rhyme subtag: bot link: rhyme CREATIVE + LEAP: + id: Double Room/Panel_leap_leap + tag: midwhite doors: Door to Cross: id: Double Room Area Doors/Door_room_4a @@ -7272,6 +7276,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 304109ca28..6c8c925138 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 918af7aba9..1fa06d2425 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -766,7 +766,6 @@ panels: BOUNCE: 445010 SCRAWL: 445011 PLUNGE: 445012 - LEAP: 445013 Rhyme Room (Circle): BIRD: 445014 LETTER: 445015 @@ -790,6 +789,7 @@ panels: GEM: 445031 INNOVATIVE (Top): 445032 INNOVATIVE (Bottom): 445033 + LEAP: 445013 Room Room: DOOR (1): 445034 DOOR (2): 445035 diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py index e466558f87..36141daa41 100644 --- a/worlds/lingo/datatypes.py +++ b/worlds/lingo/datatypes.py @@ -63,6 +63,7 @@ class Panel(NamedTuple): exclude_reduce: bool achievement: bool non_counting: bool + location_name: Optional[str] class Painting(NamedTuple): diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index 5ffedee367..c527e522fb 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -39,7 +39,7 @@ def load_location_data(): for room_name, panels in PANELS_BY_ROOM.items(): for panel_name, panel in panels.items(): - location_name = f"{room_name} - {panel_name}" + location_name = f"{room_name} - {panel_name}" if panel.location_name is None else panel.location_name classification = LocationClassification.insanity if panel.check: diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 65f27269f2..1c1f645b86 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -2,7 +2,8 @@ from dataclasses import dataclass from schema import And, Schema -from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \ + OptionGroup from .items import TRAP_ITEMS @@ -32,8 +33,8 @@ class ProgressiveColorful(DefaultOnToggle): class LocationChecks(Choice): - """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for - achievement panels and a small handful of other panels. + """Determines what locations are available. + On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. On "reduced", many of the locations that are associated with opening doors are removed. On "insanity", every individual panel in the game is a location check.""" display_name = "Location Checks" @@ -43,8 +44,10 @@ class LocationChecks(Choice): class ShuffleColors(DefaultOnToggle): - """If on, an item is added to the pool for every puzzle color (besides White). - You will need to unlock the requisite colors in order to be able to solve puzzles of that color.""" + """ + If on, an item is added to the pool for every puzzle color (besides White). + You will need to unlock the requisite colors in order to be able to solve puzzles of that color. + """ display_name = "Shuffle Colors" @@ -62,20 +65,25 @@ class ShufflePaintings(Toggle): class EnablePilgrimage(Toggle): - """If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. + """Determines how the pilgrimage works. + If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber. If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off.""" display_name = "Enable Pilgrimage" class PilgrimageAllowsRoofAccess(DefaultOnToggle): - """If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). - Otherwise, pilgrimage will be deactivated when going up the stairs.""" + """ + If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going up the stairs. + """ display_name = "Allow Roof Access for Pilgrimage" class PilgrimageAllowsPaintings(DefaultOnToggle): - """If on, you may use paintings during a pilgrimage (and you may be expected to do so). - Otherwise, pilgrimage will be deactivated when going through a painting.""" + """ + If on, you may use paintings during a pilgrimage (and you may be expected to do so). + Otherwise, pilgrimage will be deactivated when going through a painting. + """ display_name = "Allow Paintings for Pilgrimage" @@ -137,8 +145,10 @@ class Level2Requirement(Range): class EarlyColorHallways(Toggle): - """When on, a painting warp to the color hallways area will appear in the starting room. - This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.""" + """ + When on, a painting warp to the color hallways area will appear in the starting room. + This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on. + """ display_name = "Early Color Hallways" @@ -151,8 +161,10 @@ class TrapPercentage(Range): class TrapWeights(OptionDict): - """Specify the distribution of traps that should be placed into the pool. - If you don't want a specific type of trap, set the weight to zero.""" + """ + Specify the distribution of traps that should be placed into the pool. + If you don't want a specific type of trap, set the weight to zero. + """ display_name = "Trap Weights" schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS}) default = {trap_name: 1 for trap_name in TRAP_ITEMS} @@ -171,6 +183,26 @@ class DeathLink(Toggle): display_name = "Death Link" +lingo_option_groups = [ + OptionGroup("Pilgrimage", [ + EnablePilgrimage, + PilgrimageAllowsRoofAccess, + PilgrimageAllowsPaintings, + SunwarpAccess, + ShuffleSunwarps, + ]), + OptionGroup("Fine-tuning", [ + ProgressiveOrangeTower, + ProgressiveColorful, + MasteryAchievements, + Level2Requirement, + TrapPercentage, + TrapWeights, + PuzzleSkipPercentage, + ]) +] + + @dataclass class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index 10ec69be35..e40c21ce3e 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -150,8 +150,6 @@ def process_entrance(source_room, doors, room_obj): def process_panel(room_name, panel_name, panel_data): global PANELS_BY_ROOM - full_name = f"{room_name} - {panel_name}" - # required_room can either be a single room or a list of rooms. if "required_room" in panel_data: if isinstance(panel_data["required_room"], list): @@ -229,8 +227,13 @@ def process_panel(room_name, panel_name, panel_data): else: non_counting = False + if "location_name" in panel_data: + location_name = panel_data["location_name"] + else: + location_name = None + panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce, - achievement, non_counting) + achievement, non_counting, location_name) PANELS_BY_ROOM[room_name][panel_name] = panel_obj diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index 831fee2ad3..498980bb71 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -39,11 +39,12 @@ mentioned_doors = Set[] mentioned_panels = Set[] mentioned_sunwarp_entrances = Set[] mentioned_sunwarp_exits = Set[] +mentioned_paintings = Set[] door_groups = {} directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"] -panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"] door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] @@ -257,6 +258,12 @@ config.each do |room_name, room| unless paintings.include? painting["id"] then puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}" end + + if mentioned_paintings.include?(painting["id"]) then + puts "Painting #{painting["id"]} is mentioned more than once" + else + mentioned_paintings.add(painting["id"]) + end else puts "#{room_name} :::: Painting is missing an ID" end diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index 3c05e6395d..1e8437d20e 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -147,9 +147,9 @@ class L2ACSNIClient(SNIClient): snes_items_received += 1 snes_logger.info("Received %s from %s (%s) (%d/%d in list)" % ( - ctx.item_names[item.item], + ctx.item_names.lookup_in_slot(item.item), ctx.player_names[item.player], - ctx.location_names[item.location], + ctx.location_names.lookup_in_slot(item.location, item.player), snes_items_received, len(ctx.items_received))) snes_buffered_write(ctx, L2AC_RX_ADDR + 2 * (snes_items_received + 1), item_code.to_bytes(2, "little")) snes_buffered_write(ctx, L2AC_RX_ADDR, snes_items_received.to_bytes(2, "little")) diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 561429c825..6433452cef 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -65,7 +65,6 @@ class L2ACWorld(World): "Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE}, "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } - data_version: ClassVar[int] = 2 required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 728d7af861..7a21b19ef2 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -44,8 +44,6 @@ class MeritousWorld(World): location_name_to_id = location_table item_name_groups = item_groups - data_version = 2 - # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 0d8fcf4da5..73adf4ebdf 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -3,8 +3,9 @@ from typing import Dict from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ - StartInventoryPool, Toggle +from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ + PlandoConnections, Range, StartInventoryPool, Toggle, Visibility +from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS class MessengerAccessibility(Accessibility): @@ -13,6 +14,36 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") +class PortalPlando(PlandoConnections): + """ + Plando connections to be used with portal shuffle. Direction is ignored. + List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12. + The entering Portal should *not* have "Portal" appended. + For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end. + Example: + - entrance: Riviere Turquoise + exit: Wingsuit + - entrance: Sunken Shrine + exit: Sunny Day + - entrance: Searing Crags + exit: Glacial Peak Portal + """ + portals = [f"{portal} Portal" for portal in PORTALS] + shop_points = [point for points in SHOP_POINTS.values() for point in points] + checkpoints = [point for points in CHECKPOINTS.values() for point in points] + portal_entrances = PORTALS + portal_exits = portals + shop_points + checkpoints + entrances = portal_entrances + exits = portal_exits + + +# for back compatibility. To later be replaced with transition plando +class HiddenPortalPlando(PortalPlando): + visibility = Visibility.none + entrances = PortalPlando.entrances + exits = PortalPlando.exits + + class Logic(Choice): """ The level of logic to use when determining what locations in your world are accessible. @@ -205,3 +236,5 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): traps: Traps shop_price: ShopPrices shop_price_plan: PlannedShopPrices + portal_plando: PortalPlando + plando_connections: HiddenPortalPlando diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index f5603736c3..1da210cb23 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -2,8 +2,7 @@ from copy import deepcopy from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions -from worlds.generic import PlandoConnection -from .options import ShufflePortals +from Options import PlandoConnection if TYPE_CHECKING: from . import MessengerWorld @@ -207,6 +206,8 @@ REGION_ORDER = [ def shuffle_portals(world: "MessengerWorld") -> None: """shuffles the output of the portals from the main hub""" + from .options import ShufflePortals + def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" parent = out_to_parent[warp] @@ -247,7 +248,9 @@ def shuffle_portals(world: "MessengerWorld") -> None: available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) - plando = world.multiworld.plando_connections[world.player] + plando = world.options.portal_plando.value + if not plando: + plando = world.options.plando_connections.value if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index cdb5bf303f..9407097b46 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections +from .Constants import region_info class AdvancementGoal(Range): @@ -97,7 +98,19 @@ class StartingItems(OptionList): display_name = "Starting Items" +class MCPlandoConnections(PlandoConnections): + entrances = set(connection[0] for connection in region_info["default_connections"]) + exits = set(connection[1] for connection in region_info["default_connections"]) + + @classmethod + def can_connect(cls, entrance, exit): + if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]: + return False + return True + + minecraft_options: typing.Dict[str, type(Option)] = { + "plando_connections": MCPlandoConnections, "advancement_goal": AdvancementGoal, "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 343b9bad19..75e043d0cb 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -92,8 +92,6 @@ class MinecraftWorld(World): item_name_to_id = Constants.item_name_to_id location_name_to_id = Constants.location_name_to_id - data_version = 7 - def _get_mc_data(self) -> Dict[str, Any]: exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index a1cd43afba..1f08b85610 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -48,10 +48,6 @@ class MLSSClient(BizHawkClient): rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")]) rom_name = bytes([byte for byte in rom_name_bytes[0] if byte != 0]).decode("UTF-8") if not rom_name.startswith("MARIO&LUIGIUA8"): - logger.info( - "ERROR: You have opened a game that is not Mario & Luigi Superstar Saga. " - "Please make sure you are opening the correct ROM." - ) return False except UnicodeDecodeError: return False diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index eac8a37bf0..97725e728b 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -57,8 +57,6 @@ class MMBN3World(World): settings: typing.ClassVar[MMBN3Settings] topology_present = False - data_version = 1 - item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 0a8beba37b..d822a3dc38 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -519,7 +519,7 @@ Hey Vincent.|43-49|MD Plus Project|True|6|8|10| Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9| Narcissism Angel|43-51|MD Plus Project|True|1|3|6| AlterLuna|43-52|MD Plus Project|True|6|8|11|12 -Niki Tousen|43-53|MD Plus Project|True|6|8|10|11 +Niki Tousen|43-53|MD Plus Project|True|6|8|10|12 Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9| Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10| Iya Iya Iya|70-2|Rin Len's Mirrorland|False|2|4|7| @@ -538,10 +538,22 @@ Reality Show|71-2|Valentine Stage|False|5|7|10| SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8| Rose Love|71-4|Valentine Stage|True|2|4|7| Euphoria|71-5|Valentine Stage|True|1|3|6| -P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0| +P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|True|0|?|0| PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10| -How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11 -Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12 -Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10| -DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11| -Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9| +How To Make Music Game Song!|72-2|Legends of Muse Warriors|True|6|8|10|11 +Re Re|72-3|Legends of Muse Warriors|True|7|9|11|12 +Marmalade Twins|72-4|Legends of Muse Warriors|True|5|8|10| +DOMINATOR|72-5|Legends of Muse Warriors|True|7|9|11| +Teshikani TESHiKANi|72-6|Legends of Muse Warriors|True|5|7|9| +Urban Magic|73-0|Happy Otaku Pack Vol.19|True|3|5|7| +Maid's Prank|73-1|Happy Otaku Pack Vol.19|True|5|7|10| +Dance Dance Good Night Dance|73-2|Happy Otaku Pack Vol.19|True|2|4|7| +Ops Limone|73-3|Happy Otaku Pack Vol.19|True|5|8|11| +NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| +Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| +Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| +World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 +Territory Battles|74-2|CHUNITHM COURSE MUSE|True|5|7|9| +The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 +Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 +Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index b695395135..4f4f52ad2d 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -38,7 +38,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 528 # Note will probably not reach this high if any other settings are done. + range_end = 534 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 43078c5e43..af2921768d 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -34,14 +34,13 @@ class NoitaWorld(World): item_name_groups = items.item_name_groups location_name_groups = locations.location_name_groups - data_version = 2 web = NoitaWeb() def generate_early(self) -> None: if not self.multiworld.get_player_name(self.player).isascii(): raise Exception("Noita yaml's slot name has invalid character(s).") - + # Returned items will be sent over to the client def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", diff --git a/worlds/noita/options.py b/worlds/noita/options.py index f2ccbfbc4d..0fdd62365a 100644 --- a/worlds/noita/options.py +++ b/worlds/noita/options.py @@ -3,11 +3,13 @@ from dataclasses import dataclass class PathOption(Choice): - """Choose where you would like Hidden Chest and Pedestal checks to be placed. + """ + Choose where you would like Hidden Chest and Pedestal checks to be placed. Main Path includes the main 7 biomes you typically go through to get to the final boss. Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total. Main World includes the full world (excluding parallel worlds). 15 biomes total. - Note: The Collapsed Mines have been combined into the Mines as the biome is tiny.""" + Note: The Collapsed Mines have been combined into the Mines as the biome is tiny. + """ display_name = "Path Option" option_main_path = 1 option_side_path = 2 @@ -16,7 +18,9 @@ class PathOption(Choice): class HiddenChests(Range): - """Number of hidden chest checks added to the applicable biomes.""" + """ + Number of hidden chest checks added to the applicable biomes. + """ display_name = "Hidden Chests per Biome" range_start = 0 range_end = 20 @@ -24,7 +28,9 @@ class HiddenChests(Range): class PedestalChecks(Range): - """Number of checks that will spawn on pedestals in the applicable biomes.""" + """ + Number of checks that will spawn on pedestals in the applicable biomes. + """ display_name = "Pedestal Checks per Biome" range_start = 0 range_end = 20 @@ -32,15 +38,19 @@ class PedestalChecks(Range): class Traps(DefaultOnToggle): - """Whether negative effects on the Noita world are added to the item pool.""" + """ + Whether negative effects on the Noita world are added to the item pool. + """ display_name = "Traps" class OrbsAsChecks(Choice): - """Decides whether finding the orbs that naturally spawn in the world count as checks. + """ + Decides whether finding the orbs that naturally spawn in the world count as checks. The Main Path option includes only the Floating Island and Abyss Orb Room orbs. The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs. - The Main World option includes all 11 orbs.""" + The Main World option includes all 11 orbs. + """ display_name = "Orbs as Location Checks" option_no_orbs = 0 option_main_path = 1 @@ -50,10 +60,12 @@ class OrbsAsChecks(Choice): class BossesAsChecks(Choice): - """Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit. + """ + Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit. The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä. The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti. - The All Bosses option includes all 15 bosses.""" + The All Bosses option includes all 15 bosses. + """ display_name = "Bosses as Location Checks" option_no_bosses = 0 option_main_path = 1 @@ -65,11 +77,13 @@ class BossesAsChecks(Choice): # Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location. # The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending). class VictoryCondition(Choice): - """Greed is to get to the bottom, beat the boss, and win the game. + """ + Greed is to get to the bottom, beat the boss, and win the game. Pure is to get 11 orbs, grab the sampo, and bring it to the mountain altar. Peaceful is to get all 33 orbs, grab the sampo, and bring it to the mountain altar. Orbs will be added to the randomizer pool based on which victory condition you chose. - The base game orbs will not count towards these victory conditions.""" + The base game orbs will not count towards these victory conditions. + """ display_name = "Victory Condition" option_greed_ending = 0 option_pure_ending = 1 @@ -78,9 +92,11 @@ class VictoryCondition(Choice): class ExtraOrbs(Range): - """Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition. + """ + Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition. Extra orbs received past your victory condition's amount will be received as hearts instead. - Can be turned on for the Greed Ending goal, but will only really make it harder.""" + Can be turned on for the Greed Ending goal, but will only really make it harder. + """ display_name = "Extra Orbs" range_start = 0 range_end = 10 @@ -88,8 +104,10 @@ class ExtraOrbs(Range): class ShopPrice(Choice): - """Reduce the costs of Archipelago items in shops. - By default, the price of Archipelago items matches the price of wands at that shop.""" + """ + Reduce the costs of Archipelago items in shops. + By default, the price of Archipelago items matches the price of wands at that shop. + """ display_name = "Shop Price Reduction" option_full_price = 100 option_25_percent_off = 75 @@ -98,10 +116,17 @@ class ShopPrice(Choice): default = 100 +class NoitaDeathLink(DeathLink): + """ + When you die, everyone dies. Of course, the reverse is true too. + You can disable this in the in-game mod options. + """ + + @dataclass class NoitaOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool - death_link: DeathLink + death_link: NoitaDeathLink bad_effects: Traps victory_condition: VictoryCondition path_option: PathOption diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 2543cdc715..daf072adb5 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,7 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -29,6 +30,11 @@ class TrackRandomRange(Range): raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.") +class OoTPlandoConnections(PlandoConnections): + entrances = set([connection[1][0] for connection in entrance_shuffle_table]) + exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2]) + + class Logic(Choice): """Set the logic used for the generator. Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. @@ -1277,6 +1283,7 @@ class LogicTricks(OptionList): # All options assembled into a single dict oot_options: typing.Dict[str, type(Option)] = { + "plando_connections": OoTPlandoConnections, "logic_rules": Logic, "logic_no_night_tokens_without_suns_song": NightTokens, **open_options, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index d9ee63850e..34b3935fec 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from .Cosmetics import patch_cosmetics from Utils import get_options from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys, Accessibility +from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld @@ -150,8 +150,6 @@ class OOTWorld(World): location_name_to_id = location_name_to_id web = OOTWeb() - data_version = 3 - required_client_version = (0, 4, 0) item_name_groups = { @@ -203,6 +201,8 @@ class OOTWorld(World): option_value = bool(result) elif isinstance(result, VerifyKeys): option_value = result.value + elif isinstance(result, PlandoConnections): + option_value = result.value else: option_value = result.current_key setattr(self, option_name, option_value) diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index be66fa3a8a..44227d4bec 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -48,7 +48,6 @@ class Overcooked2World(World): web = Overcooked2Web() required_client_version = (0, 3, 8) topology_present: bool = False - data_version = 3 item_name_to_id = item_name_to_id item_id_to_name = item_id_to_name diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index f0bed12577..e967b2039b 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -5,6 +5,8 @@ - When you blacklist species from wild encounters and turn on dexsanity, blacklisted species are not added as locations and won't show up in the wild. Previously they would be forced to show up exactly once. - Added support for some new autotracking events. +- Updated option descriptions. +- Added `full` alias for `100` on TM and HM compatibility options. ### Fixes diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 6225350a5e..aa4f6ccf75 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -87,7 +87,6 @@ class PokemonEmeraldWorld(World): item_name_groups = ITEM_GROUPS location_name_groups = LOCATION_GROUPS - data_version = 2 required_client_version = (0, 4, 6) badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] @@ -175,26 +174,26 @@ class PokemonEmeraldWorld(World): # In race mode we don't patch any item location information into the ROM if self.multiworld.is_race and not self.options.remote_items: logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.", - self.player, self.multiworld.player_name[self.player]) + self.player, self.player_name) self.options.remote_items.value = Toggle.option_true if self.options.goal == Goal.option_legendary_hunt: # Prevent turning off all legendary encounters if len(self.options.allowed_legendary_hunt_encounters.value) == 0: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) " - "needs to allow at least one legendary encounter when goal is legendary hunt.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at " + "least one legendary encounter when goal is legendary hunt.") # Prevent setting the number of required legendaries higher than the number of enabled legendaries if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value): logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed " "legendary encounters. Reducing to number of allowed encounters.", self.player, - self.multiworld.player_name[self.player]) + self.player_name) self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value) # Require random wild encounters if dexsanity is enabled if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: - raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.multiworld.player_name[self.player]}) must " - "not leave wild encounters vanilla if enabling dexsanity.") + raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild " + "encounters vanilla if enabling dexsanity.") # If badges or HMs are vanilla, Norman locks you from using Surf, # which means you're not guaranteed to be able to reach Fortree Gym, @@ -224,7 +223,7 @@ class PokemonEmeraldWorld(World): if self.options.norman_count.value > max_norman_count: logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " - "other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player)) + "other settings. Reducing to 4.", self.player, self.player_name) self.options.norman_count.value = max_norman_count def create_regions(self) -> None: @@ -589,7 +588,7 @@ class PokemonEmeraldWorld(World): randomize_opponent_parties(self) randomize_starters(self) - patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) + patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4")) write_tokens(self, patch) @@ -608,7 +607,7 @@ class PokemonEmeraldWorld(World): if self.options.dexsanity: from collections import defaultdict - spoiler_handle.write(f"\n\nWild Pokemon ({self.multiworld.player_name[self.player]}):\n\n") + spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") species_maps = defaultdict(set) for map in self.modified_maps.values(): @@ -670,7 +669,7 @@ class PokemonEmeraldWorld(World): def modify_multidata(self, multidata: Dict[str, Any]): import base64 - multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name] def fill_slot_data(self) -> Dict[str, Any]: slot_data = self.options.as_dict( diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 3a99a09acb..a830957e9c 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -452,7 +452,7 @@ class PokemonEmeraldClient(BizHawkClient): self.death_counter = times_whited_out elif times_whited_out > self.death_counter: await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! " - f"{ctx.player_names[ctx.slot]} whited out!") + f"{ctx.player_names[ctx.slot]} whited out!") self.ignore_next_death_link = True self.death_counter = times_whited_out diff --git a/worlds/pokemon_emerald/docs/setup_es.md b/worlds/pokemon_emerald/docs/setup_es.md index 28c3a4a01a..1d3721862a 100644 --- a/worlds/pokemon_emerald/docs/setup_es.md +++ b/worlds/pokemon_emerald/docs/setup_es.md @@ -14,51 +14,51 @@ Una vez que hayas instalado BizHawk, abre `EmuHawk.exe` y cambia las siguientes `NLua+KopiLua` a `Lua+LuaInterface`, luego reinicia EmuHawk. (Si estás usando BizHawk 2.9, puedes saltar este paso.) - En `Config > Customize`, activa la opción "Run in background" para prevenir desconexiones del cliente mientras la aplicación activa no sea EmuHawk. -- Abre el archivo `.gba` en EmuHawk y luego ve a `Config > Controllers…` para configurar los controles. Si no puedes +- Abre el archivo `.gba` en EmuHawk y luego ve a `Config > Controllers…` para configurar los controles. Si no puedes hacer clic en `Controllers…`, debes abrir cualquier ROM `.gba` primeramente. -- Considera limpiar tus macros y atajos en `Config > Hotkeys…` si no quieres usarlas de manera intencional. Para +- Considera limpiar tus macros y atajos en `Config > Hotkeys…` si no quieres usarlas de manera intencional. Para limpiarlas, selecciona el atajo y presiona la tecla Esc. ## Software Opcional -- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar con -[PopTracker](https://github.com/black-sliver/PopTracker/releases) +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), para usar +con [PopTracker](https://github.com/black-sliver/PopTracker/releases) ## Generando y Parcheando el Juego -1. Crea tu archivo de configuración (YAML). Puedes hacerlo en +1. Crea tu archivo de configuración (YAML). Puedes hacerlo en [Página de Opciones de Pokémon Emerald](../../../games/Pokemon%20Emerald/player-options). -2. Sigue las instrucciones generales de Archipelago para [Generar un juego] -(../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para ti. Tu archivo -de parche tendrá la extensión de archivo`.apemerald`. +2. Sigue las instrucciones generales de Archipelago para +[Generar un juego](../../Archipelago/setup/en#generating-a-game). Esto generará un archivo de salida (output file) para +ti. Tu archivo de parche tendrá la extensión de archivo `.apemerald`. 3. Abre `ArchipelagoLauncher.exe` 4. Selecciona "Open Patch" en el lado derecho y elige tu archivo de parcheo. 5. Si esta es la primera vez que vas a parchear, se te pedirá que selecciones la ROM sin parchear. 6. Un archivo parcheado con extensión `.gba` será creado en el mismo lugar que el archivo de parcheo. -7. La primera vez que abras un archivo parcheado con el BizHawk Client, se te preguntará donde está localizado +7. La primera vez que abras un archivo parcheado con el BizHawk Client, se te preguntará donde está localizado `EmuHawk.exe` en tu instalación de BizHawk. -Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el -cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras -implementaciones de Archipelago, continúa usando BizHawk como tu emulador +Si estás jugando una seed Single-Player y no te interesa el auto-tracking o las pistas, puedes parar aquí, cierra el +cliente, y carga la ROM ya parcheada en cualquier emulador. Pero para partidas multi-worlds y para otras +implementaciones de Archipelago, continúa usando BizHawk como tu emulador. ## Conectando con el Servidor -Por defecto, al abrir un archivo parcheado, se harán de manera automática 1-5 pasos. Aun así, ten en cuenta lo +Por defecto, al abrir un archivo parcheado, se harán de manera automática 1-5 pasos. Aun así, ten en cuenta lo siguiente en caso de que debas cerrar y volver a abrir la ventana en mitad de la partida por algún motivo. -1. Pokémon Emerald usa el Archipelago BizHawk Client. Si el cliente no se encuentra abierto al abrir la rom +1. Pokémon Emerald usa el Archipelago BizHawk Client. Si el cliente no se encuentra abierto al abrir la rom parcheada, puedes volver a abrirlo desde el Archipelago Launcher. 2. Asegúrate que EmuHawk está corriendo la ROM parcheada. 3. En EmuHawk, ve a `Tools > Lua Console`. Debes tener esta ventana abierta mientras juegas. 4. En la ventana de Lua Console, ve a `Script > Open Script…`. 5. Ve a la carpeta donde está instalado Archipelago y abre `data/lua/connector_bizhawk_generic.lua`. -6. El emulador y el cliente eventualmente se conectarán uno con el otro. La ventana de BizHawk Client indicará que te +6. El emulador y el cliente eventualmente se conectarán uno con el otro. La ventana de BizHawk Client indicará que te has conectado y reconocerá Pokémon Emerald. -7. Para conectar el cliente con el servidor, ingresa la dirección y el puerto de la sala (ej. `archipelago.gg:38281`) +7. Para conectar el cliente con el servidor, ingresa la dirección y el puerto de la sala (ej. `archipelago.gg:38281`) en el campo de texto que se encuentra en la parte superior del cliente y haz click en Connect. -Ahora deberías poder enviar y recibir ítems. Debes seguir estos pasos cada vez que quieras reconectarte. Es seguro +Ahora deberías poder enviar y recibir ítems. Debes seguir estos pasos cada vez que quieras reconectarte. Es seguro jugar de manera offline; se sincronizará todo cuando te vuelvas a conectar. ## Tracking Automático @@ -70,5 +70,5 @@ Pokémon Emerald tiene un Map Tracker completamente funcional que soporta auto-t 2. Coloca la carpeta del Tracker en la carpeta packs/ dentro de la carpeta de instalación del PopTracker. 3. Abre PopTracker, y carga el Pack de Pokémon Emerald Map Tracker. 4. Para utilizar el auto-tracking, haz click en el símbolo "AP" que se encuentra en la parte superior. -5. Entra la dirección del Servidor de Archipelago (la misma a la que te conectaste para jugar), nombre del jugador, y +5. Entra la dirección del Servidor de Archipelago (la misma a la que te conectaste para jugar), nombre del jugador, y contraseña (deja vacío este campo en caso de no utilizar contraseña). diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 978f9d3dcd..e05b5d96ac 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -3,7 +3,7 @@ Option definitions for Pokemon Emerald """ from dataclasses import dataclass -from Options import (Choice, DeathLink, DefaultOnToggle, TextChoice, OptionSet, NamedRange, Range, Toggle, FreeText, +from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, PerGameCommonOptions) from .data import data @@ -11,12 +11,12 @@ from .data import data class Goal(Choice): """ - Determines what your goal is to consider the game beaten + Determines what your goal is to consider the game beaten. - Champion: Become the champion and enter the hall of fame - Steven: Defeat Steven in Meteor Falls - Norman: Defeat Norman in Petalburg Gym - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) + - Champion: Become the champion and enter the hall of fame + - Steven: Defeat Steven in Meteor Falls + - Norman: Defeat Norman in Petalburg Gym + - Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters) """ display_name = "Goal" default = 0 @@ -28,11 +28,11 @@ class Goal(Choice): class RandomizeBadges(Choice): """ - Adds Badges to the pool + Adds Badges to the pool. - Vanilla: Gym leaders give their own badge - Shuffle: Gym leaders give a random badge - Completely Random: Badges can be found anywhere + - Vanilla: Gym leaders give their own badge + - Shuffle: Gym leaders give a random badge + - Completely Random: Badges can be found anywhere """ display_name = "Randomize Badges" default = 2 @@ -43,11 +43,11 @@ class RandomizeBadges(Choice): class RandomizeHms(Choice): """ - Adds HMs to the pool + Adds HMs to the pool. - Vanilla: HMs are at their vanilla locations - Shuffle: HMs are shuffled among vanilla HM locations - Completely Random: HMs can be found anywhere + - Vanilla: HMs are at their vanilla locations + - Shuffle: HMs are shuffled among vanilla HM locations + - Completely Random: HMs can be found anywhere """ display_name = "Randomize HMs" default = 2 @@ -58,50 +58,51 @@ class RandomizeHms(Choice): class RandomizeKeyItems(DefaultOnToggle): """ - Adds most key items to the pool. These are usually required to unlock - a location or region (e.g. Devon Scope, Letter, Basement Key) + Adds most key items to the pool. + + These are usually required to unlock a location or region (e.g. Devon Scope, Letter, Basement Key). """ display_name = "Randomize Key Items" class RandomizeBikes(Toggle): """ - Adds the mach bike and acro bike to the pool + Adds the Mach Bike and Acro Bike to the pool. """ display_name = "Randomize Bikes" class RandomizeEventTickets(Toggle): """ - Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove + Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove. """ display_name = "Randomize Event Tickets" class RandomizeRods(Toggle): """ - Adds fishing rods to the pool + Adds fishing rods to the pool. """ display_name = "Randomize Fishing Rods" class RandomizeOverworldItems(DefaultOnToggle): """ - Adds items on the ground with a Pokeball sprite to the pool + Adds items on the ground with a Pokeball sprite to the pool. """ display_name = "Randomize Overworld Items" class RandomizeHiddenItems(Toggle): """ - Adds hidden items to the pool + Adds hidden items to the pool. """ display_name = "Randomize Hidden Items" class RandomizeNpcGifts(Toggle): """ - Adds most gifts received from NPCs to the pool (not including key items or HMs) + Adds most gifts received from NPCs to the pool (not including key items or HMs). """ display_name = "Randomize NPC Gifts" @@ -115,7 +116,9 @@ class RandomizeBerryTrees(Toggle): class Dexsanity(Toggle): """ - Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). + Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.). Only wild encounters are considered logical access to a species. + + Blacklisting wild encounters removes the dexsanity location. Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. @@ -126,21 +129,20 @@ class Dexsanity(Toggle): class Trainersanity(Toggle): """ - Defeating a trainer for the first time gives you an item. Trainers are no longer missable. + Defeating a trainer gives you an item. - Trainers no longer give you money for winning. Each trainer adds a valuable item (nugget, stardust, etc.) to the pool. + Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. """ display_name = "Trainersanity" class ItemPoolType(Choice): """ - Determines which non-progression items get put into the item pool + Determines which non-progression items get put into the item pool. - Shuffled: Item pool consists of shuffled vanilla items - Diverse Balanced: Item pool consists of random items approximately proportioned - according to what they're replacing (i.e. more pokeballs, fewer X items, etc.) - Diverse: Item pool consists of uniformly random (non-unique) items + - Shuffled: Item pool consists of shuffled vanilla items + - Diverse Balanced: Item pool consists of random items approximately proportioned according to what they're replacing + - Diverse: Item pool consists of uniformly random (non-unique) items """ display_name = "Item Pool Type" default = 0 @@ -151,14 +153,14 @@ class ItemPoolType(Choice): class HiddenItemsRequireItemfinder(DefaultOnToggle): """ - The Itemfinder is logically required to pick up hidden items + The Itemfinder is logically required to pick up hidden items. """ display_name = "Require Itemfinder" class DarkCavesRequireFlash(Choice): """ - Determines whether HM05 Flash is logically required to navigate a dark cave + Determines whether HM05 Flash is logically required to navigate a dark cave. """ display_name = "Require Flash" default = 3 @@ -170,10 +172,10 @@ class DarkCavesRequireFlash(Choice): class EliteFourRequirement(Choice): """ - Sets the requirements to challenge the elite four + Sets the requirements to challenge the elite four. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gyms """ display_name = "Elite Four Requirement" default = 0 @@ -183,7 +185,7 @@ class EliteFourRequirement(Choice): class EliteFourCount(Range): """ - Sets the number of badges/gyms required to challenge the elite four + Sets the number of badges/gyms required to challenge the elite four. """ display_name = "Elite Four Count" range_start = 0 @@ -193,10 +195,10 @@ class EliteFourCount(Range): class NormanRequirement(Choice): """ - Sets the requirements to challenge the Petalburg Gym + Sets the requirements to challenge the Petalburg Gym. - Badges: Obtain some number of badges - Gyms: Defeat some number of gyms + - Badges: Obtain some number of badges + - Gyms: Defeat some number of gym leaders """ display_name = "Norman Requirement" default = 0 @@ -206,7 +208,7 @@ class NormanRequirement(Choice): class NormanCount(Range): """ - Sets the number of badges/gyms required to challenge the Petalburg Gym + Sets the number of badges/gyms required to challenge the Petalburg Gym. """ display_name = "Norman Count" range_start = 0 @@ -216,14 +218,16 @@ class NormanCount(Range): class LegendaryHuntCatch(Toggle): """ - Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. Defeated legendaries can be respawned by defeating the Elite 4. + Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. + + Defeated legendaries can be respawned by defeating the Elite 4. """ display_name = "Legendary Hunt Requires Catching" class LegendaryHuntCount(Range): """ - Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal + Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal. """ display_name = "Legendary Hunt Count" range_start = 1 @@ -235,24 +239,12 @@ class AllowedLegendaryHuntEncounters(OptionSet): """ Sets which legendary encounters can contribute to the Legendary Hunt goal. - Latios will always be the roamer. Latias will always be at Southern Island. + Latias will always be at Southern Island. Latios will always be the roamer. The TV broadcast describing the roamer gives you "seen" info for Latios. - Possible values are: - "Groudon" - "Kyogre" - "Rayquaza" - "Latios" - "Latias" - "Regirock" - "Registeel" - "Regice" - "Ho-Oh" - "Lugia" - "Deoxys" - "Mew" + The braille puzzle in Sealed Chamber gives you "seen" info for Wailord and Relicanth. The move tutor in Fortree City always teaches Dig. """ display_name = "Allowed Legendary Hunt Encounters" - valid_keys = frozenset([ + valid_keys = [ "Groudon", "Kyogre", "Rayquaza", @@ -265,19 +257,19 @@ class AllowedLegendaryHuntEncounters(OptionSet): "Lugia", "Deoxys", "Mew", - ]) + ] default = valid_keys.copy() class RandomizeWildPokemon(Choice): """ - Randomizes wild pokemon encounters (grass, caves, water, fishing) + Randomizes wild pokemon encounters (grass, caves, water, fishing). - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Wild encounters are unchanged + - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst + - Match Type: Wild pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Wild Pokemon" default = 0 @@ -294,21 +286,21 @@ class WildEncounterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Wild Encounter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeStarters(Choice): """ - Randomizes the starter pokemon in Professor Birch's bag + Randomizes the starter pokemon in Professor Birch's bag. - Vanilla: Starters are unchanged - Match Base Stats: Starters are replaced with species with approximately the same bst - Match Type: Starters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Starters are unchanged + - Match Base Stats: Starters are replaced with species with approximately the same bst + - Match Type: Starters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Starters" default = 0 @@ -325,21 +317,21 @@ class StarterBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Starter Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Parties are unchanged + - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst + - Match Type: Trainer pokemon are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Trainer Parties" default = 0 @@ -356,10 +348,10 @@ class TrainerPartyBlacklist(OptionSet): May be overridden if enforcing other restrictions in combination with this blacklist is impossible. - Use "_Legendaries" as a shortcut for legendary pokemon. + Use "_Legendaries" as a shortcut for all legendary pokemon. """ display_name = "Trainer Party Blacklist" - valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"} + valid_keys = ["_Legendaries"] + sorted([species.label for species in data.species.values()]) class ForceFullyEvolved(Range): @@ -376,12 +368,12 @@ class RandomizeLegendaryEncounters(Choice): """ Randomizes legendary encounters (Rayquaza, Regice, Latias, etc.). The roamer will always be Latios during legendary hunts. - Vanilla: Legendary encounters are unchanged - Shuffle: Legendary encounters are shuffled between each other - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst - Match Type: Legendary encounters are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Legendary encounters are unchanged + - Shuffle: Legendary encounters are shuffled between each other + - Match Base Stats: Legendary encounters are replaced with species with approximately the same bst + - Match Type: Legendary encounters are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Legendary Encounters" default = 0 @@ -397,12 +389,12 @@ class RandomizeMiscPokemon(Choice): """ Randomizes non-legendary static encounters. May grow to include other pokemon like trades or gifts. - Vanilla: Species are unchanged - Shuffle: Species are shuffled between each other - Match Base Stats: Species are replaced with species with approximately the same bst - Match Type: Species are replaced with species that share a type with the original - Match Base Stats and Type: Apply both Match Base Stats and Match Type - Completely Random: There are no restrictions + - Vanilla: Species are unchanged + - Shuffle: Species are shuffled between each other + - Match Base Stats: Species are replaced with species with approximately the same bst + - Match Type: Species are replaced with species that share a type with the original + - Match Base Stats and Type: Apply both Match Base Stats and Match Type + - Completely Random: There are no restrictions """ display_name = "Randomize Misc Pokemon" default = 0 @@ -418,10 +410,10 @@ class RandomizeTypes(Choice): """ Randomizes the type(s) of every pokemon. Each species will have the same number of types. - Vanilla: Types are unchanged - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) - Completely Random: Each species has its type(s) randomized - Follow Evolutions: Types are randomized per evolution line instead of per species + - Vanilla: Types are unchanged + - Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type) + - Completely Random: Each species has its type(s) randomized + - Follow Evolutions: Types are randomized per evolution line instead of per species """ display_name = "Randomize Types" default = 0 @@ -435,10 +427,9 @@ class RandomizeAbilities(Choice): """ Randomizes abilities of every species. Each species will have the same number of abilities. - Vanilla: Abilities are unchanged - Completely Random: Each species has its abilities randomized - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability - when evolving, the random ability will also be retained + - Vanilla: Abilities are unchanged + - Completely Random: Each species has its abilities randomized + - Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability when evolving, the random ability will also be retained """ display_name = "Randomize Abilities" default = 0 @@ -449,22 +440,21 @@ class RandomizeAbilities(Choice): class AbilityBlacklist(OptionSet): """ - A list of abilities which no pokemon should have if abilities are randomized. - For example, you could exclude Wonder Guard and Arena Trap like this: - ["Wonder Guard", "Arena Trap"] + Prevent species from being given these abilities. + + Has no effect if abilities are not randomized. """ display_name = "Ability Blacklist" - valid_keys = frozenset([ability.label for ability in data.abilities]) + valid_keys = sorted([ability.label for ability in data.abilities]) class LevelUpMoves(Choice): """ - Randomizes the moves a pokemon learns when they reach a level where they would learn a move. - Your starter is guaranteed to have a usable damaging move. + Randomizes the moves a pokemon learns when they reach a level where they would learn a move. Your starter is guaranteed to have a usable damaging move. - Vanilla: Learnset is unchanged - Randomized: Moves are randomized - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 + - Vanilla: Learnset is unchanged + - Randomized: Moves are randomized + - Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1 """ display_name = "Level Up Moves" default = 0 @@ -487,8 +477,7 @@ class MoveMatchTypeBias(Range): class MoveNormalTypeBias(Range): """ - After it has been decided that a move will not be forced to match types, sets the probability that a learned move - will be forced to be the Normal type. + After it has been decided that a move will not be forced to match types, sets the probability that a learned move will be forced to be the Normal type. If a move is not forced to be Normal, it will be completely random. """ @@ -500,41 +489,51 @@ class MoveNormalTypeBias(Range): class MoveBlacklist(OptionSet): """ - A list of moves which should be excluded from learnsets, TMs, and move tutors. + Prevents species from learning these moves via learnsets, TMs, and move tutors. + + HM moves are already banned. """ display_name = "Move Blacklist" - valid_keys = frozenset(data.move_labels.keys()) + valid_keys = sorted(data.move_labels.keys()) class HmCompatibility(NamedRange): """ - Sets the percent chance that a given HM is compatible with a species + Sets the percent chance that a given HM is compatible with a species. + + Some opponents like gym leaders are allowed to use HMs. This option can affect the moves they know. """ display_name = "HM Compatibility" default = -1 range_start = 50 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorCompatibility(NamedRange): """ - Sets the percent chance that a given TM or move tutor is compatible with a species + Sets the percent chance that a given TM or move tutor is compatible with a species. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Compatibility" default = -1 range_start = 0 range_end = 100 special_range_names = { - "vanilla": -1 + "vanilla": -1, + "full": 100, } class TmTutorMoves(Toggle): """ - Randomizes the moves taught by TMs and move tutors + Randomizes the moves taught by TMs and move tutors. + + Some opponents like gym leaders are allowed to use TMs. This option can affect the moves they know. """ display_name = "TM/Tutor Moves" @@ -562,7 +561,7 @@ class MinCatchRate(Range): class GuaranteedCatch(Toggle): """ - Every throw is guaranteed to catch a wild pokemon + Every throw is guaranteed to catch a wild pokemon. """ display_name = "Guaranteed Catch" @@ -571,14 +570,16 @@ class NormalizeEncounterRates(Toggle): """ Make every slot on an encounter table approximately equally likely. - This does NOT mean every species is equally likely. But it will make rarer encounters less rare overall. + This does NOT mean each species is equally likely. In the vanilla game, each species may occupy more than one slot, and slots vary in probability. + + Species will still occupy the same slots as vanilla, but the slots will be equally weighted. The minimum encounter rate will be 8% (higher in water). """ display_name = "Normalize Encounter Rates" class ExpModifier(Range): """ - Multiplies gained experience by a percentage + Multiplies gained experience by a percentage. 100 is default 50 is half @@ -593,14 +594,14 @@ class ExpModifier(Range): class BlindTrainers(Toggle): """ - Causes trainers to not start a battle with you unless you talk to them + Trainers will not start a battle with you unless you talk to them. """ display_name = "Blind Trainers" class PurgeSpinners(Toggle): """ - Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run + Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run. """ display_name = "Purge Spinners" @@ -613,9 +614,9 @@ class MatchTrainerLevels(Choice): This is a pseudo-replacement for a level cap and makes every trainer battle a fair fight while still allowing you to level up. - Off: The vanilla experience - Additive: The modifier you apply to your team is a flat bonus - Multiplicative: The modifier you apply to your team is a percent bonus + - Off: The vanilla experience + - Additive: The modifier you apply to your team is a flat bonus + - Multiplicative: The modifier you apply to your team is a percent bonus """ display_name = "Match Trainer Levels" default = 0 @@ -629,10 +630,10 @@ class MatchTrainerLevelsBonus(Range): A level bonus (or penalty) to apply to your team when matching an opponent's levels. When the match trainer levels option is "additive", this value is added to your team's levels during a battle. - For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. + For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team. When the match trainer levels option is "multiplicative", this is a percent bonus. - For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. + For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team. """ display_name = "Match Trainer Levels Modifier" range_start = -100 @@ -643,7 +644,9 @@ class MatchTrainerLevelsBonus(Range): class DoubleBattleChance(Range): """ The percent chance that a trainer with more than 1 pokemon will be converted into a double battle. + If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon. + They can be battled by talking to them no matter what. """ display_name = "Double Battle Chance" @@ -654,7 +657,7 @@ class DoubleBattleChance(Range): class BetterShops(Toggle): """ - Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city) + Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city). """ display_name = "Better Shops" @@ -663,19 +666,10 @@ class RemoveRoadblocks(OptionSet): """ Removes specific NPCs that normally stand in your way until certain events are completed. - This can open up the world a bit and make your playthrough less linear, but careful how many you remove; it may make too much of your world accessible upon receiving Surf. - - Possible values are: - "Route 110 Aqua Grunts" - "Route 112 Magma Grunts" - "Route 119 Aqua Grunts" - "Safari Zone Construction Workers" - "Lilycove City Wailmer" - "Aqua Hideout Grunts" - "Seafloor Cavern Aqua Grunt" + This can open up the world a bit and make your playthrough less linear, but be careful how many you remove; it may make too much of your world accessible upon receiving Surf. """ display_name = "Remove Roadblocks" - valid_keys = frozenset([ + valid_keys = [ "Route 110 Aqua Grunts", "Route 112 Magma Grunts", "Route 119 Aqua Grunts", @@ -683,12 +677,13 @@ class RemoveRoadblocks(OptionSet): "Lilycove City Wailmer", "Aqua Hideout Grunts", "Seafloor Cavern Aqua Grunt", - ]) + ] class ExtraBoulders(Toggle): """ Places strength boulders on Route 115 which block access to Meteor Falls from the beach. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Extra Boulders" @@ -697,6 +692,7 @@ class ExtraBoulders(Toggle): class ExtraBumpySlope(Toggle): """ Adds a bumpy slope to Route 115 which allows access to Meteor Falls if you have the Acro Bike. + This aims to take some power away from Surf by adding a new way to exit the Rustboro area. """ display_name = "Extra Bumpy Slope" @@ -705,6 +701,7 @@ class ExtraBumpySlope(Toggle): class ModifyRoute118(Toggle): """ Changes the layout of Route 118 so that it must be crossed with the Acro Bike instead of Surf. + This aims to take some power away from Surf by restricting how much it allows you to access. """ display_name = "Modify Route 118" @@ -712,14 +709,14 @@ class ModifyRoute118(Toggle): class FreeFlyLocation(Toggle): """ - Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items) + Enables flying to one random location (excluding cities reachable with no items). """ display_name = "Free Fly Location" -class HmRequirements(TextChoice): +class HmRequirements(Choice): """ - Sets the requirements to use HMs outside of battle + Sets the requirements to use HMs outside of battle. """ display_name = "HM Requirements" default = 0 @@ -729,7 +726,7 @@ class HmRequirements(TextChoice): class TurboA(Toggle): """ - Holding A will advance most text automatically + Holding A will advance most text automatically. """ display_name = "Turbo A" @@ -738,9 +735,9 @@ class ReceiveItemMessages(Choice): """ Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld. - All: Every item shows a message - Progression: Only progression items show a message - None: All items are added to your bag silently (badges will still show) + - All: Every item shows a message + - Progression: Only progression items show a message + - None: All items are added to your bag silently (badges will still show). """ display_name = "Receive Item Messages" default = 0 @@ -754,6 +751,7 @@ class RemoteItems(Toggle): Instead of placing your own items directly into the ROM, all items are received from the server, including items you find for yourself. This enables co-op of a single slot and recovering more items after a lost save file (if you're so unlucky). + But it changes pickup behavior slightly and requires connection to the server to receive any items. """ display_name = "Remote Items" @@ -781,9 +779,10 @@ class WonderTrading(DefaultOnToggle): Wonder trading NEVER affects logic. - Certain aspects of a pokemon species are per-game, not per-pokemon. - As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Certain aspects of a pokemon species are per-game, not per-pokemon. As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on. + Receiving a pokemon this way does not mark it as found in your pokedex. + Trade evolutions do not evolve this way; they retain their modified methods (level ups and item use). """ display_name = "Wonder Trading" @@ -795,6 +794,7 @@ class EasterEgg(FreeText): All secret phrases are something that could be a trendy phrase in Dewford Town. They are case insensitive. """ + display_name = "Easter Egg" default = "EMERALD SECRET" diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 09203bab8f..968a103ccd 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -184,7 +184,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat location.item.name ) for trainer in alternates) - player_name_ids: Dict[str, int] = {world.multiworld.player_name[world.player]: 0} + player_name_ids: Dict[str, int] = {world.player_name: 0} item_name_offsets: Dict[str, int] = {} next_item_name_offset = 0 for i, (flag, item_player, item_name) in enumerate(sorted(location_info, key=lambda t: t[0])): @@ -208,7 +208,7 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat struct.pack(" 0 and \ diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 8e4eda09e1..e96cd44712 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -39,7 +39,6 @@ class RaftWorld(World): location_name_to_id = locations_lookup_name_to_id option_definitions = raft_options - data_version = 2 required_client_version = (0, 3, 4) def create_items(self): diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index c5a8d71b5d..eb65769954 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -35,7 +35,6 @@ class RLWorld(World): game = "Rogue Legacy" option_definitions = rl_options topology_present = True - data_version = 4 required_client_version = (0, 3, 5) web = RLWeb() diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 5afdb797e7..b6a1901a8d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -44,7 +44,6 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - data_version = 9 required_client_version = (0, 4, 5) web = RiskOfWeb() total_revivals: int diff --git a/worlds/sa2b/Options.py b/worlds/sa2b/Options.py index b298042692..438e59de5e 100644 --- a/worlds/sa2b/Options.py +++ b/worlds/sa2b/Options.py @@ -1,18 +1,26 @@ -import typing +from dataclasses import dataclass -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions class Goal(Choice): """ Determines the goal of the seed + Biolizard: Finish Cannon's Core and defeat the Biolizard and Finalhazard + Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone + Finalhazard Chaos Emerald Hunt: Find the Seven Chaos Emeralds and reach Green Hill Zone, then defeat Finalhazard + Grand Prix: Win every race in Kart Race Mode (all standard levels are disabled) + Boss Rush: Beat all of the bosses in the Boss Rush, ending with Finalhazard + Cannon's Core Boss Rush: Beat Cannon's Core, then beat all of the bosses in the Boss Rush, ending with Finalhazard + Boss Rush Chaos Emerald Hunt: Find the Seven Chaos Emeralds, then beat all of the bosses in the Boss Rush, ending with Finalhazard + Chaos Chao: Raise a Chaos Chao to win """ display_name = "Goal" @@ -46,9 +54,13 @@ class MissionShuffle(Toggle): class BossRushShuffle(Choice): """ Determines how bosses in Boss Rush Mode are shuffled + Vanilla: Bosses appear in the Vanilla ordering + Shuffled: The same bosses appear, but in a random order + Chaos: Each boss is randomly chosen separately (one will always be King Boom Boo) + Singularity: One boss is chosen and placed in every slot (one will always be replaced with King Boom Boo) """ display_name = "Boss Rush Shuffle" @@ -196,9 +208,13 @@ class Keysanity(Toggle): class Whistlesanity(Choice): """ Determines whether whistling at various spots grants checks + None: No Whistle Spots grant checks + Pipes: Whistling at Pipes grants checks (97 Locations) + Hidden: Whistling at Hidden Whistle Spots grants checks (32 Locations) + Both: Whistling at both Pipes and Hidden Whistle Spots grants checks (129 Locations) """ display_name = "Whistlesanity" @@ -228,8 +244,9 @@ class Omosanity(Toggle): class Animalsanity(Toggle): """ Determines whether unique counts of animals grant checks. - ALL animals must be collected in a single run of a mission to get all checks. (421 Locations) + + ALL animals must be collected in a single run of a mission to get all checks. """ display_name = "Animalsanity" @@ -237,8 +254,11 @@ class Animalsanity(Toggle): class KartRaceChecks(Choice): """ Determines whether Kart Race Mode grants checks + None: No Kart Races grant checks + Mini: Each Kart Race difficulty must be beaten only once + Full: Every Character must separately beat each Kart Race difficulty """ display_name = "Kart Race Checks" @@ -271,8 +291,11 @@ class NumberOfLevelGates(Range): class LevelGateDistribution(Choice): """ Determines how levels are distributed between level gate regions + Early: Earlier regions will have more levels than later regions + Even: Levels will be evenly distributed between all regions + Late: Later regions will have more levels than earlier regions """ display_name = "Level Gate Distribution" @@ -296,7 +319,9 @@ class LevelGateCosts(Choice): class MaximumEmblemCap(Range): """ Determines the maximum number of emblems that can be in the item pool. + If fewer available locations exist in the pool than this number, the number of available locations will be used instead. + Gate and Cannon's Core costs will be calculated based off of that number. """ display_name = "Max Emblem Cap" @@ -321,9 +346,13 @@ class RequiredRank(Choice): class ChaoRaceDifficulty(Choice): """ Determines the number of Chao Race difficulty levels included. Easier difficulty settings means fewer Chao Race checks + None: No Chao Races have checks + Beginner: Beginner Races + Intermediate: Beginner, Challenge, Hero, and Dark Races + Expert: Beginner, Challenge, Hero, Dark and Jewel Races """ display_name = "Chao Race Difficulty" @@ -350,9 +379,10 @@ class ChaoKarateDifficulty(Choice): class ChaoStadiumChecks(Choice): """ Determines which Chao Stadium activities grant checks + All: Each individual race and karate fight grants a check - Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and - 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty) + + Prize: Only the races which grant Chao Toys grant checks (final race of each Beginner and Jewel cup, 4th, 8th, and 12th Challenge Races, 2nd and 4th Hero and Dark Races, final fight of each Karate difficulty) """ display_name = "Chao Stadium Checks" option_all = 0 @@ -374,6 +404,7 @@ class ChaoStats(Range): class ChaoStatsFrequency(Range): """ Determines how many levels in each Chao Stat grant checks (up to the maximum set in the `chao_stats` option) + `1` means every level is included, `2` means every other level is included, `3` means every third, and so on """ display_name = "Chao Stats Frequency" @@ -408,8 +439,11 @@ class ChaoKindergarten(Choice): """ Determines whether learning the lessons from the Kindergarten Classroom grants checks (WARNING: VERY SLOW) + None: No Kindergarten classes have checks + Basics: One class from each category (Drawing, Dance, Song, and Instrument) is a check (4 Locations) + Full: Every class is a check (23 Locations) """ display_name = "Chao Kindergarten Checks" @@ -443,8 +477,8 @@ class BlackMarketUnlockCosts(Choice): class BlackMarketPriceMultiplier(Range): """ Determines how many rings the Black Market items cost - The base ring costs of items in the Black Market range from 50-100, - and are then multiplied by this value + + The base ring costs of items in the Black Market range from 50-100, and are then multiplied by this value """ display_name = "Black Market Price Multiplier" range_start = 0 @@ -469,7 +503,9 @@ class ChaoEntranceRandomization(Toggle): class RequiredCannonsCoreMissions(Choice): """ Determines how many Cannon's Core missions must be completed (for Biolizard or Cannon's Core goals) + First: Only the first mission must be completed + All Active: All active Cannon's Core missions must be completed """ display_name = "Required Cannon's Core Missions" @@ -665,8 +701,11 @@ class CannonsCoreMission5(DefaultOnToggle): class RingLoss(Choice): """ How taking damage is handled + Classic: You lose all of your rings when hit + Modern: You lose 20 rings when hit + OHKO: You die immediately when hit (NOTE: Some Hard Logic tricks may require damage boosts!) """ display_name = "Ring Loss" @@ -693,9 +732,13 @@ class RingLink(Toggle): class SADXMusic(Choice): """ Whether the randomizer will include Sonic Adventure DX Music in the music pool + SA2B: Only SA2B music will be played + SADX: Only SADX music will be played + Both: Both SA2B and SADX music will be played + NOTE: This option requires the player to own a PC copy of SADX and to follow the addition steps in the setup guide. """ display_name = "SADX Music" @@ -715,9 +758,13 @@ class SADXMusic(Choice): class MusicShuffle(Choice): """ What type of Music Shuffle is used + None: No music is shuffled. + Levels: Level music is shuffled. + Full: Level, Menu, and Additional music is shuffled. + Singularity: Level, Menu, and Additional music is all replaced with a single random song. """ display_name = "Music Shuffle Type" @@ -731,10 +778,15 @@ class MusicShuffle(Choice): class VoiceShuffle(Choice): """ What type of Voice Shuffle is used + None: No voices are shuffled. + Shuffled: Voices are shuffled. + Rude: Voices are shuffled, but some are replaced with rude words. + Chao: All voices are replaced with chao sounds. + Singularity: All voices are replaced with a single random voice. """ display_name = "Voice Shuffle Type" @@ -768,7 +820,9 @@ class Narrator(Choice): class LogicDifficulty(Choice): """ What set of Upgrade Requirement logic to use + Standard: The logic assumes the "intended" usage of Upgrades to progress through levels + Hard: Some simple skips or sequence breaks may be required """ display_name = "Logic Difficulty" @@ -777,96 +831,195 @@ class LogicDifficulty(Choice): default = 0 -sa2b_options: typing.Dict[str, type(Option)] = { - "goal": Goal, +sa2b_option_groups = [ + OptionGroup("General Options", [ + Goal, + BossRushShuffle, + LogicDifficulty, + RequiredRank, + MaximumEmblemCap, + RingLoss, + ]), + OptionGroup("Stages", [ + MissionShuffle, + EmblemPercentageForCannonsCore, + RequiredCannonsCoreMissions, + NumberOfLevelGates, + LevelGateCosts, + LevelGateDistribution, + ]), + OptionGroup("Sanity Options", [ + Keysanity, + Whistlesanity, + Beetlesanity, + Omosanity, + Animalsanity, + KartRaceChecks, + ]), + OptionGroup("Chao", [ + BlackMarketSlots, + BlackMarketUnlockCosts, + BlackMarketPriceMultiplier, + ChaoRaceDifficulty, + ChaoKarateDifficulty, + ChaoStadiumChecks, + ChaoAnimalParts, + ChaoStats, + ChaoStatsFrequency, + ChaoStatsStamina, + ChaoStatsHidden, + ChaoKindergarten, + ShuffleStartingChaoEggs, + ChaoEntranceRandomization, + ]), + OptionGroup("Junk and Traps", [ + JunkFillPercentage, + TrapFillPercentage, + OmochaoTrapWeight, + TimestopTrapWeight, + ConfusionTrapWeight, + TinyTrapWeight, + GravityTrapWeight, + ExpositionTrapWeight, + IceTrapWeight, + SlowTrapWeight, + CutsceneTrapWeight, + ReverseTrapWeight, + PongTrapWeight, + MinigameTrapDifficulty, + ]), + OptionGroup("Speed Missions", [ + SpeedMissionCount, + SpeedMission2, + SpeedMission3, + SpeedMission4, + SpeedMission5, + ]), + OptionGroup("Mech Missions", [ + MechMissionCount, + MechMission2, + MechMission3, + MechMission4, + MechMission5, + ]), + OptionGroup("Hunt Missions", [ + HuntMissionCount, + HuntMission2, + HuntMission3, + HuntMission4, + HuntMission5, + ]), + OptionGroup("Kart Missions", [ + KartMissionCount, + KartMission2, + KartMission3, + KartMission4, + KartMission5, + ]), + OptionGroup("Cannon's Core Missions", [ + CannonsCoreMissionCount, + CannonsCoreMission2, + CannonsCoreMission3, + CannonsCoreMission4, + CannonsCoreMission5, + ]), + OptionGroup("Aesthetics", [ + SADXMusic, + MusicShuffle, + VoiceShuffle, + Narrator, + ]), +] - "mission_shuffle": MissionShuffle, - "boss_rush_shuffle": BossRushShuffle, - "keysanity": Keysanity, - "whistlesanity": Whistlesanity, - "beetlesanity": Beetlesanity, - "omosanity": Omosanity, - "animalsanity": Animalsanity, - "kart_race_checks": KartRaceChecks, +@dataclass +class SA2BOptions(PerGameCommonOptions): + goal: Goal + boss_rush_shuffle: BossRushShuffle + logic_difficulty: LogicDifficulty + required_rank: RequiredRank + max_emblem_cap: MaximumEmblemCap + ring_loss: RingLoss - "logic_difficulty": LogicDifficulty, - "required_rank": RequiredRank, - "required_cannons_core_missions": RequiredCannonsCoreMissions, + mission_shuffle: MissionShuffle + required_cannons_core_missions: RequiredCannonsCoreMissions + emblem_percentage_for_cannons_core: EmblemPercentageForCannonsCore + number_of_level_gates: NumberOfLevelGates + level_gate_distribution: LevelGateDistribution + level_gate_costs: LevelGateCosts - "emblem_percentage_for_cannons_core": EmblemPercentageForCannonsCore, - "number_of_level_gates": NumberOfLevelGates, - "level_gate_distribution": LevelGateDistribution, - "level_gate_costs": LevelGateCosts, - "max_emblem_cap": MaximumEmblemCap, + keysanity: Keysanity + whistlesanity: Whistlesanity + beetlesanity: Beetlesanity + omosanity: Omosanity + animalsanity: Animalsanity + kart_race_checks: KartRaceChecks - "chao_race_difficulty": ChaoRaceDifficulty, - "chao_karate_difficulty": ChaoKarateDifficulty, - "chao_stadium_checks": ChaoStadiumChecks, - "chao_stats": ChaoStats, - "chao_stats_frequency": ChaoStatsFrequency, - "chao_stats_stamina": ChaoStatsStamina, - "chao_stats_hidden": ChaoStatsHidden, - "chao_animal_parts": ChaoAnimalParts, - "chao_kindergarten": ChaoKindergarten, - "black_market_slots": BlackMarketSlots, - "black_market_unlock_costs": BlackMarketUnlockCosts, - "black_market_price_multiplier": BlackMarketPriceMultiplier, - "shuffle_starting_chao_eggs": ShuffleStartingChaoEggs, - "chao_entrance_randomization": ChaoEntranceRandomization, + black_market_slots: BlackMarketSlots + black_market_unlock_costs: BlackMarketUnlockCosts + black_market_price_multiplier: BlackMarketPriceMultiplier + chao_race_difficulty: ChaoRaceDifficulty + chao_karate_difficulty: ChaoKarateDifficulty + chao_stadium_checks: ChaoStadiumChecks + chao_animal_parts: ChaoAnimalParts + chao_stats: ChaoStats + chao_stats_frequency: ChaoStatsFrequency + chao_stats_stamina: ChaoStatsStamina + chao_stats_hidden: ChaoStatsHidden + chao_kindergarten: ChaoKindergarten + shuffle_starting_chao_eggs: ShuffleStartingChaoEggs + chao_entrance_randomization: ChaoEntranceRandomization - "junk_fill_percentage": JunkFillPercentage, - "trap_fill_percentage": TrapFillPercentage, - "omochao_trap_weight": OmochaoTrapWeight, - "timestop_trap_weight": TimestopTrapWeight, - "confusion_trap_weight": ConfusionTrapWeight, - "tiny_trap_weight": TinyTrapWeight, - "gravity_trap_weight": GravityTrapWeight, - "exposition_trap_weight": ExpositionTrapWeight, - #"darkness_trap_weight": DarknessTrapWeight, - "ice_trap_weight": IceTrapWeight, - "slow_trap_weight": SlowTrapWeight, - "cutscene_trap_weight": CutsceneTrapWeight, - "reverse_trap_weight": ReverseTrapWeight, - "pong_trap_weight": PongTrapWeight, - "minigame_trap_difficulty": MinigameTrapDifficulty, + junk_fill_percentage: JunkFillPercentage + trap_fill_percentage: TrapFillPercentage + omochao_trap_weight: OmochaoTrapWeight + timestop_trap_weight: TimestopTrapWeight + confusion_trap_weight: ConfusionTrapWeight + tiny_trap_weight: TinyTrapWeight + gravity_trap_weight: GravityTrapWeight + exposition_trap_weight: ExpositionTrapWeight + #darkness_trap_weight: DarknessTrapWeight + ice_trap_weight: IceTrapWeight + slow_trap_weight: SlowTrapWeight + cutscene_trap_weight: CutsceneTrapWeight + reverse_trap_weight: ReverseTrapWeight + pong_trap_weight: PongTrapWeight + minigame_trap_difficulty: MinigameTrapDifficulty - "sadx_music": SADXMusic, - "music_shuffle": MusicShuffle, - "voice_shuffle": VoiceShuffle, - "narrator": Narrator, - "ring_loss": RingLoss, + sadx_music: SADXMusic + music_shuffle: MusicShuffle + voice_shuffle: VoiceShuffle + narrator: Narrator - "speed_mission_count": SpeedMissionCount, - "speed_mission_2": SpeedMission2, - "speed_mission_3": SpeedMission3, - "speed_mission_4": SpeedMission4, - "speed_mission_5": SpeedMission5, + speed_mission_count: SpeedMissionCount + speed_mission_2: SpeedMission2 + speed_mission_3: SpeedMission3 + speed_mission_4: SpeedMission4 + speed_mission_5: SpeedMission5 - "mech_mission_count": MechMissionCount, - "mech_mission_2": MechMission2, - "mech_mission_3": MechMission3, - "mech_mission_4": MechMission4, - "mech_mission_5": MechMission5, + mech_mission_count: MechMissionCount + mech_mission_2: MechMission2 + mech_mission_3: MechMission3 + mech_mission_4: MechMission4 + mech_mission_5: MechMission5 - "hunt_mission_count": HuntMissionCount, - "hunt_mission_2": HuntMission2, - "hunt_mission_3": HuntMission3, - "hunt_mission_4": HuntMission4, - "hunt_mission_5": HuntMission5, + hunt_mission_count: HuntMissionCount + hunt_mission_2: HuntMission2 + hunt_mission_3: HuntMission3 + hunt_mission_4: HuntMission4 + hunt_mission_5: HuntMission5 - "kart_mission_count": KartMissionCount, - "kart_mission_2": KartMission2, - "kart_mission_3": KartMission3, - "kart_mission_4": KartMission4, - "kart_mission_5": KartMission5, + kart_mission_count: KartMissionCount + kart_mission_2: KartMission2 + kart_mission_3: KartMission3 + kart_mission_4: KartMission4 + kart_mission_5: KartMission5 - "cannons_core_mission_count": CannonsCoreMissionCount, - "cannons_core_mission_2": CannonsCoreMission2, - "cannons_core_mission_3": CannonsCoreMission3, - "cannons_core_mission_4": CannonsCoreMission4, - "cannons_core_mission_5": CannonsCoreMission5, + cannons_core_mission_count: CannonsCoreMissionCount + cannons_core_mission_2: CannonsCoreMission2 + cannons_core_mission_3: CannonsCoreMission3 + cannons_core_mission_4: CannonsCoreMission4 + cannons_core_mission_5: CannonsCoreMission5 - "ring_link": RingLink, - "death_link": DeathLink, -} + ring_link: RingLink + death_link: DeathLink diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 7d77aebc4c..f7d1ca72b0 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -3,20 +3,20 @@ import math import logging from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from worlds.AutoWorld import WebWorld, World + +from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \ + all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits +from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups, \ eggs_table, fruits_table, seeds_table, hats_table, animals_table, chaos_drives_table from .Locations import SA2BLocation, all_locations, setup_locations, chao_animal_event_location_table, black_market_location_table -from .Options import sa2b_options +from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions +from .Names import ItemName, LocationName +from .Options import SA2BOptions, sa2b_option_groups from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \ gate_0_blacklist_regions from .Rules import set_rules -from .Names import ItemName, LocationName -from .AestheticData import chao_name_conversion, sample_chao_names, totally_real_item_names, \ - all_exits, all_destinations, multi_rooms, single_rooms, room_to_exits_map, exit_to_room_map, valid_kindergarten_exits -from worlds.AutoWorld import WebWorld, World -from .GateBosses import get_gate_bosses, get_boss_rush_bosses, get_boss_name -from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions -import Patch class SA2BWeb(WebWorld): @@ -30,8 +30,9 @@ class SA2BWeb(WebWorld): "setup/en", ["RaspberrySpaceJam", "PoryGone", "Entiss"] ) - + tutorials = [setup_en] + option_groups = sa2b_option_groups def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld): @@ -54,9 +55,9 @@ class SA2BWorld(World): Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth. """ game: str = "Sonic Adventure 2 Battle" - option_definitions = sa2b_options + options_dataclass = SA2BOptions + options: SA2BOptions topology_present = False - data_version = 7 item_name_groups = item_groups item_name_to_id = {name: data.code for name, data in item_table.items()} diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 96b3ddc66b..ac9ccfffcd 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -244,10 +244,10 @@ class StarcraftClientProcessor(ClientCommandProcessor): self.formatted_print(f" [u]{faction.name}[/u] ") for item_id in categorized_items[faction]: - item_name = self.ctx.item_names[item_id] + item_name = self.ctx.item_names.lookup_in_slot(item_id) received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) matching_children = [child for child in received_child_items - if item_matches_filter(self.ctx.item_names[child])] + if item_matches_filter(self.ctx.item_names.lookup_in_slot(child))] received_items_of_this_type = items_received.get(item_id, []) item_is_match = item_matches_filter(item_name) if item_is_match or len(matching_children) > 0: @@ -966,8 +966,8 @@ def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool: return kerrigan_level >= 35 elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion: total_missions = len(ctx.mission_id_to_location_ids) - completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations - for mission_id in ctx.mission_id_to_location_ids]) + completed = sum((mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations + for mission_id in ctx.mission_id_to_location_ids) return completed >= (total_missions / 2) elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_item: codes = [item.item for item in ctx.items_received] @@ -1165,7 +1165,7 @@ def request_unfinished_missions(ctx: SC2Context) -> None: objectives = set(ctx.locations_for_mission(mission)) if objectives: remaining_objectives = objectives.difference(ctx.checked_locations) - unfinished_locations[mission] = [ctx.location_names[location_id] for location_id in remaining_objectives] + unfinished_locations[mission] = [ctx.location_names.lookup_in_slot(location_id) for location_id in remaining_objectives] else: unfinished_locations[mission] = [] diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 167583fd1e..f9dcfc18eb 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -269,7 +269,7 @@ class SC2Manager(GameManager): for loc in self.ctx.locations_for_mission(mission_name): if loc in self.ctx.missing_locations: count += 1 - locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names[loc]) + locations[lookup_location_id_to_type[loc]].append(self.ctx.location_names.lookup_in_slot(loc)) plando_locations = [] for plando_loc in self.ctx.plando_locations: diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index 59c6fe9001..ec8a447d93 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -22,7 +22,7 @@ from .MissionTables import MissionInfo, SC2Campaign, lookup_name_to_mission, SC2 class Starcraft2WebWorld(WebWorld): - setup = Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld", "English", @@ -31,7 +31,16 @@ class Starcraft2WebWorld(WebWorld): ["TheCondor", "Phaneros"] ) - tutorials = [setup] + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Neocerber"] + ) + + tutorials = [setup_en, setup_fr] class SC2World(World): diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index 784d711319..06464e3cd2 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -1,5 +1,8 @@ # Starcraft 2 +## Game page in other languages: +* [Français](/games/Starcraft%202/info/fr) + ## What does randomization do to this game? The following unlocks are randomized as items: @@ -39,7 +42,7 @@ The goal is to beat the final mission in the mission order. The yaml configurati ## Which of my items can be in another player's world? By default, any of StarCraft 2's items (specified above) can be in another player's world. See the -[Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) +[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this. ## Unique Local Commands diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md new file mode 100644 index 0000000000..4fcc8e689b --- /dev/null +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -0,0 +1,95 @@ +# *StarCraft 2* + +## Quel est l'effet de la *randomization* sur ce jeu ? + +Les éléments qui suivent sont les *items* qui sont *randomized* et qui doivent être débloqués pour être utilisés dans +le jeu: +1. La capacité de produire des unités, excepté les drones/probes/scv. +2. Des améliorations spécifiques à certaines unités incluant quelques combinaisons qui ne sont pas disponibles dans les +campagnes génériques, comme le fait d'avoir les deux types d'évolution en même temps pour une unité *Zerg* et toutes +les améliorations de la *Spear of Adun* simultanément pour les *Protoss*. +3. L'accès aux améliorations génériques des unités, e.g. les améliorations d'attaque et d'armure. +4. D'autres améliorations diverses telles que les améliorations de laboratoire et les mercenaires pour les *Terran*, +les niveaux et les améliorations de Kerrigan pour les *Zerg*, et les améliorations de la *Spear of Adun* pour les +*Protoss*. +5. Avoir des *minerals*, du *vespene gas*, et du *supply* au début de chaque mission. + +Les *items* sont trouvés en accomplissant du progrès dans les catégories suivantes: +* Terminer des missions +* Réussir des objectifs supplémentaires (e.g., récolter le matériel pour les recherches dans *Wings of Liberty*) +* Atteindre des étapes importantes dans la mission, e.g. réussir des sous-objectifs +* Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission +*Devil's Playground* + +Ces catégories, outre la première, peuvent être désactivées dans les options du jeu. +Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont +accomplies. + +Quand vous recevez un *item*, il devient immédiatement disponible, même pendant une mission, et vous serez avertis via +la boîte de texte situé dans le coin en haut à droite de *StarCraft 2*. +L'acquisition d'un *item* est aussi indiquée dans le client d'Archipelago. + +Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet +*StarCraft 2 Launcher*. +Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus. +De plus, les points de progression tels que les crédits ou la Solarite ne sont pas utilisés dans *StarCraft 2 +Archipelago*. + +## Quel est le but de ce jeu quand il est *randomized*? + +Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.). +Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées. + +## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2* + +1. Certaines des missions ont plus de *vespene geysers* pour permettre l'utilisation d'une plus grande variété d'unités. +2. Plusieurs unités et améliorations ont été ajoutées sous la forme d*items*. +Ils proviennent de la version *co-op*, *melee*, des autres campagnes, d'expansions ultérieures, de *Brood War*, ou de +l'imagination des développeurs de *StarCraft 2 Archipelago*. +3. Les structures de production, e.g. *Factory*, *Starport*, *Robotics Facility*, and *Stargate*, n'ont plus +d'exigences technologiques. +4. Les missions avec la race *Zerg* ont été modifiées pour que les joueurs débuttent avec un *Lair* lorsqu'elles +commençaient avec une *Hatchery*. +5. Les désavantages des améliorations ont été enlevés, e.g. *automated refinery* qui coûte plus cher ou les *tech +reactors* qui prennent plus de temps à construire. +6. La collision des unités dans les couloirs de la mission *Enemy Within* a été ajustée pour permettre des unités +plus larges de les traverser sans être coincés dans des endroits étranges. +7. Plusieurs *bugs* du jeu original ont été corrigés. + +## Quels sont les *items* qui peuvent être dans le monde d'un autre joueur? + +Par défaut, tous les *items* de *StarCraft 2 Archipelago* (voir la section précédente) peuvent être dans le monde d'un +autre joueur. +Consulter [*Advanced YAML Guide*](/tutorial/Archipelago/advanced_settings/en) pour savoir comment +changer ça. + +## Commandes du client qui sont uniques à ce jeu + +Les commandes qui suivent sont seulement disponibles uniquement pour le client de *StarCraft 2 Archipelago*. +Vous pouvez les afficher en utilisant la commande `/help` dans le client de *StarCraft 2 Archipelago*. +Toutes ces commandes affectent seulement le client où elles sont utilisées. + +* `/download_data` Télécharge les versions les plus récentes des fichiers pour jouer à *StarCraft 2 Archipelago*. +Les fichiers existants vont être écrasés. +* `/difficulty [difficulty]` Remplace la difficulté choisie pour le monde. + * Les options sont *casual*, *normal*, *hard*, et *brutal*. +* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde. + * Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*. +* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable. + * Les options de *faction*: raynor, kerrigan, primal, protoss, nova. + * Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*, +*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*. +* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*. + * Si la commande est lancée sans option, la liste des options qui sont modifiables va être affichée. + * Les options qui peuvent être changées avec cette commande incluent sauter les cinématiques automatiquement, la +présence de Kerrigan dans les missions, la disponibilité de la *Spear of Adun*, la quantité de ressources +supplémentaires données au début des missions, la capacité de contrôler les alliées IA, etc. +* `/disable_mission_check` Désactive les requit pour lancer les missions. +Cette option a pour but de permettre de jouer en mode coopératif en permettant à un joueur de jouer à la prochaine +mission de la chaîne qu'un autre joueur est en train d'entamer. +* `/play [mission_id]` Lance la mission correspondant à l'identifiant donné. +* `/available` Affiche les missions qui sont présentement accessibles. +* `/unfinished` Affiche les missions qui sont présentement accessibles et dont certains des objectifs permettant +l'accès à un *item* n'ont pas été accomplis. +* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la +détection automatique de cette dernière échoue. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 4956109778..991ed57e87 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -23,18 +23,20 @@ Yaml files are configuration files that tell Archipelago how you'd like your gam When you're setting up a multiworld, every world needs its own yaml file. There are three basic ways to get a yaml: -* You can go to the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. -* You can generate a template, either by downloading it from the [Player Options](https://archipelago.gg/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. +* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. +* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. * You can ask someone else to share their yaml to use it for yourself or adjust it as you wish. Remember the name you enter in the options page or in the yaml file, you'll need it to connect later! -Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information. +Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information. ### Common yaml questions #### How do I know I set my yaml up correctly? -The simplest way to check is to test it out. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. +The simplest way to check is to use the website [validator](/check). + +You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. #### What does Progression Balancing do? @@ -64,9 +66,15 @@ start_inventory: An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax. -#### How do I know the exact names of items? +#### How do I know the exact names of items and locations? -You can look up a complete list if item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/). +The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2. + +You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. +This page also contains supplementary information of each item. +However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. + +As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client. ## How do I join a MultiWorld game? @@ -86,7 +94,7 @@ specific description of what's going wrong and attach your log file to your mess ## Running in macOS -To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. ## Running in Linux diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md new file mode 100644 index 0000000000..bb6c35bce1 --- /dev/null +++ b/worlds/sc2/docs/setup_fr.md @@ -0,0 +1,214 @@ +# Guide d'installation du *StarCraft 2 Randomizer* + +Ce guide contient les instructions pour installer et dépanner le client de *StarCraft 2 Archipelago*, ainsi que des +indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago* et comment modifier ce dernier. + +## Logiciels requis + +- [*StarCraft 2*](https://starcraft2.com/en-us/) +- [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Comment est-ce que j'installe ce *randomizer*? + +1. Installer *StarCraft 2* et Archipelago en suivant les instructions indiquées dans les liens précédents. Le client de +*StarCraft 2 Archipelago* est téléchargé par le programme d'installation d'Archipelago. + - Les utilisateurs de Linux devraient aussi suivre les instructions qui se retrouvent à la fin de cette page +(["Exécuter sous Linux"](#exécuter-sous-linux)). + - Notez que votre jeu *StarCraft 2* doit être en anglais pour fonctionner avec Archipelago. +2. Exécuter `ArchipelagoStarcraft2Client.exe`. + - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions qui se trouvent à +["Exécuter sous macOS"](#exécuter-sous-macos). +3. Dans le client de *StarCraft 2 Archipelago*, écrire la commande `/download_data`. Cette commande va lancer +l'installation des fichiers qui sont nécessaires pour jouer à *StarCraft 2 Archipelago*. + +## Où est-ce que j'obtiens le fichier de configuration (i.e., le *yaml*) pour ce jeu? + +Un fichier dans le format *yaml* est utilisé pour communiquer à Archipelago comment vous voulez que votre jeu soit +*randomized*. +Ce dernier est nécessaire même si vous voulez utiliser les options par défaut. +L'approche usuelle pour générer un *multiworld* consiste à avoir un fichier *yaml* par monde. + +Il y a trois approches pour obtenir un fichier *yaml* pour *StarCraft 2 Randomizer*: +* Vous pouvez aller à la page [*Player options*](/games/Starcraft%202/player-options) qui vous permet de définir vos +choix via une interface graphique et ensuite télécharger le *yaml* correspondant à ces choix. +* Vous pouvez obtenir le modèle de base en le téléchargeant à la page +[*Player options*](/games/Starcraft%202/player-options) ou en cliquant sur *Generate template* après avoir exécuté le +*Launcher* d'Archipelago (i.e., `ArchipelagoLauncher.exe`). Ce modèle de base inclut une description pour chacune des +options et vous n'avez qu'à modifier les options dans un éditeur de texte de votre choix. +* Vous pouvez demander à quelqu'un d'autre de partager un de ces fichiers *yaml* pour l'utiliser ou l'ajuster à vos +préférences. + +Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml* +puisque vous en aurez besoin pour vous connecter à votre monde! + +Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de +certaines unités ou de leurs améliorations. +Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières. + +Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago, +veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml). + +### Questions récurrentes à propos du fichier *yaml* +#### Comment est-ce que je sais que mon *yaml* est bien défini? + +La manière la plus simple de valider votre *yaml* est d'utiliser le +[système de validation](/check) du site web. + +Vous pouvez aussi le tester en tentant de générer un *multiworld* avec votre *yaml*. +Pour faire ça, sauvegardez votre *yaml* dans le dossier `Players/` de votre installation d'Archipelago et exécutez +`ArchipelagoGenerate.exe`. +Si votre *yaml* est bien défini, vous devriez voir un nouveau fichier, avec l'extension `.zip`, apparaître dans le +dossier `output/` de votre installation d'Archipelago. +Il est recommandé de lancer `ArchipelagoGenerate.exe` via un terminal afin que vous puissiez voir les messages générés +par le logiciel, ce qui va inclure toutes erreurs qui ont eu lieu et le nom de fichier généré. +Si vous n'appréciez pas le fait d'utiliser un terminal, vous pouvez aussi regarder le fichier *log* qui va être produit +dans le dossier `logs/`. + +#### À quoi sert l'option *Progression Balancing*? + +Pour *Starcraft 2*, cette option ne fait pas grand-chose. +Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items* +dans les *spheres*. +Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont +plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes. +Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à +donc peu d'influence sur la progression dans *StarCraft 2*. +Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à-d le définir à +zéro, pour *Starcraft 2*. + + +#### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*? + +Vous pouvez lire sur la syntaxe des conteneurs dans le format *yaml* à la page +[*YAML specification*](https://yaml.org/spec/1.2.2/#21-collections). +Pour les listes, chaque *item* doit être sur sa propre ligne et doit être précédé par un trait d'union. + +```yaml +excluded_items: + - Battlecruiser + - Drop-Pods (Kerrigan Tier 7) +``` + +Une liste vide est représentée par une paire de crochets: `[]`. +Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette +syntaxe. + +#### Comment est-ce que je fais pour avoir des *items* dès le départ? + +L'option *starting inventory* est un *map* et non une liste. +Ainsi, elle permet de spécifier le nombre de chaque *item* avec lequel vous allez commencer. +Sa syntaxe consiste à indiquer le nom de l'*item*, suivi par un deux-points, puis par un espace et enfin par le nombre +désiré de cet *item*. + +```yaml +start_inventory: + Micro-Filtering: 1 + Additional Starting Vespene: 5 +``` + +Un *map* vide est représenté par une paire d'accolades: `{}`. +Il s'agit de la valeur par défaut dans le modèle de base, ce qui devrait vous aider à apprendre à utiliser cette +syntaxe. + +#### Comment est-ce que je fais pour connaître le nom des *items* et des *locations* dans *StarCraft 2 Archipelago*? + +La page [*datapackage*](/datapackage) d'Archipelago liste l'ensemble des *items* et des *locations* de tous les jeux +que le site web prend en charge actuellement, dont ceux de *StarCraft 2*. + +Vous trouverez aussi la liste complète des *items* de *StarCraft 2 Archipelago* à la page +[*Icon Repository*](https://matthewmarinets.github.io/ap_sc2_icons/). +Notez que cette page contient diverses informations supplémentaires sur chacun des *items*. +Cependant, l'information présente dans cette dernière peut différer de celle du *datapackage* d'Archipelago +puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui +n'ont peut-être pas encore été inclus dans le site web d'Archipelago. + +## Comment est-ce que je peux joindre un *MultiWorld*? + +1. Exécuter `ArchipelagoStarcraft2Client.exe`. + - Uniquement pour cette étape, les utilisateurs de macOS devraient plutôt suivre les instructions à la page +["Exécuter sous macOS"](#exécuter-sous-macos). +2. Entrer la commande `/connect [server ip]`. + - Si le *MultiWorld* est hébergé via un siteweb, l'IP du server devrait être indiqué dans le haut de la page de +votre *room*. +3. Inscrivez le nom de joueur spécifié dans votre *yaml* lorsque vous y êtes invité. +4. Si le serveur a un mot de passe, l'inscrire lorsque vous y êtes invité. +5. Une fois connecté, aller sur l'onglet *StarCraft 2 Launcher* dans le client. Dans cet onglet, vous devriez trouver +toutes les missions de votre monde. Les missions qui ne sont pas disponibles présentement auront leur texte dans une +nuance de gris. Vous n'avez qu'à cliquer une des missions qui est disponible pour la commencer! + +## *StarCraft 2* ne démarre pas quand je tente de commencer une mission + +Pour commencer, regarder le fichier *log* pour trouver le problème (ce dernier devrait être dans +`[Archipelago Directory]/logs/SC2Client.txt`). +Si vous ne comprenez pas le problème avec le fichier *log*, visitez notre +[*Discord*](https://discord.com/invite/8Z65BR2) pour demander de l'aide dans le forum *tech-support*. +Dans votre message, veuillez inclure une description détaillée de ce qui ne marche pas et ajouter en pièce jointe le +fichier *log*. + +## Mon profil de raccourcis clavier n'est pas disponibles quand je joue à *StarCraft 2 Archipelago* + +Pour que votre profil de raccourcis clavier fonctionne dans Archipelago, vous devez copier votre fichier de raccourcis +qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Documents/StarCraft II/Hotkeys`. +Si le dossier n'existe pas, créez-le. + +Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes. +Lancez *Starcraft 2* via l'application *Battle.net*. +Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil +personnalisé et acceptez. +Vous n'aurez besoin de faire ça qu'une seule fois. + +## Exécuter sous macOS + +Pour exécuter *StarCraft 2* via Archipelago sous macOS, vous devez exécuter le client à partir de la source +comme indiqué ici: [*macOS Guide*](/tutorial/Archipelago/mac/en). +Notez que pour lancer le client, vous devez exécuter la commande `python3 Starcraft2Client.py`. + +## Exécuter sous Linux + +Pour exécuter *StarCraft 2* via Archipelago sous Linux, vous allez devoir installer le jeu avec *Wine* et ensuite +exécuter le client d'Archipelago pour Linux. + +Confirmez que vous avez installé *StarCraft 2* via *Wine* et que vous avez suivi les +[instructions d'installation](#comment-est-ce-que-j'installe-ce-randomizer?) pour ajouter les *Maps* et les *Data +files* nécessairent pour *StarCraft 2 Archipelago* au bon endroit. +Vous n'avez pas besoin de copier les fichiers `.dll`. +Si vous avez des difficultés pour installer ou exécuter *StarCraft 2* sous Linux, il est recommandé d'utiliser le +logiciel *Lutris*. + +Copier ce qui suit dans un fichier avec l'extension `.sh`, en prenant soin de définir les variables **WINE** et +**SC2PATH** avec les bons chemins et de définir **PATH_TO_ARCHIPELAGO** avec le chemin vers le dossier qui contient le +*AppImage* si ce dernier n'est pas dans le même dossier que ce script. + +```sh +# Permet au client de savoir que SC2 est exécuté via Wine +export SC2PF=WineLinux +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +# À_CHANGER Remplacer le chemin avec celui qui correspond à la version de Wine utilisé pour exécuter SC2 +export WINE="/usr/bin/wine" + +# À_CHANGER Remplacer le chemin par celui qui indique où StarCraft II est installé +export SC2PATH="/home/user/Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II/" + +# À_CHANGER Indiquer le dossier qui contient l'AppImage d'Archipelago +PATH_TO_ARCHIPELAGO= + +# Obtiens la dernière version de l'AppImage de Archipelago dans le dossier PATH_TO_ARCHIPELAGO. +# Si PATH_TO_ARCHIPELAGO n'est pas défini, la valeur par défaut est le dossier qui contient ce script. +ARCHIPELAGO="$(ls ${PATH_TO_ARCHIPELAGO:-$(dirname $0)}/Archipelago_*.AppImage | sort -r | head -1)" + +# Lance le client de Archipelago +$ARCHIPELAGO Starcraft2Client +``` + +Pour une installation via Lutris, vous pouvez exécuter `lutris -l` pour obtenir l'identifiant numérique de votre +installation *StarCraft II* et ensuite exécuter la commande suivante, en remplacant **${ID}** pour cet identifiant +numérique. + + lutris lutris:rungameid/${ID} --output-script sc2.sh + +Cette commande va définir toutes les variables d'environnement nécessaires pour exécuter *StarCraft 2* dans un script, +incluant le chemin vers l'exécutable *Wine* que Lutris utilise. +Après ça, vous pouvez enlever la ligne qui permet de démarrer *Battle.Net* et copier le code décrit plus haut dans le +script produit. + diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py index 3e0430f024..470b061c4b 100644 --- a/worlds/shorthike/__init__.py +++ b/worlds/shorthike/__init__.py @@ -28,7 +28,6 @@ class ShortHikeWorld(World): game = "A Short Hike" web = ShortHikeWeb() - data_version = 2 item_name_to_id = {item["name"]: item["id"] for item in item_table} location_name_to_id = {loc["name"]: loc["id"] for loc in location_table} diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 7c97f743c5..6d6dd08ba5 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -123,7 +123,7 @@ class SMSNIClient(SNIClient): location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -151,9 +151,8 @@ class SMSNIClient(SNIClient): snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 7f12bf484c..826b144779 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -99,7 +99,6 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 3 option_definitions = sm_options settings: typing.ClassVar[SMSettings] diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 0e944aa4ab..833ae56ca3 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,7 +35,6 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 33a74b3dc8..85bb3fe1ee 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -448,7 +448,7 @@ class SMWSNIClient(SNIClient): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) snes_logger.info( f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) @@ -499,15 +499,16 @@ class SMWSNIClient(SNIClient): if recv_index < len(ctx.items_received): item = ctx.items_received[recv_index] recv_index += 1 + sending_game = ctx.slot_info[item.player].game logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) if self.should_show_message(ctx, item): if item.item != 0xBC0012 and item.item not in trap_rom_data: # Don't send messages for Boss Tokens - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -515,7 +516,7 @@ class SMWSNIClient(SNIClient): snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index&0xFF, (recv_index>>8)&0xFF])) if item.item in trap_rom_data: - item_name = ctx.item_names[item.item] + item_name = ctx.item_names.lookup_in_slot(item.item) player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) @@ -596,7 +597,7 @@ class SMWSNIClient(SNIClient): for loc_id in ctx.checked_locations: if loc_id not in ctx.locations_checked: ctx.locations_checked.add(loc_id) - loc_name = ctx.location_names[loc_id] + loc_name = ctx.location_names.lookup_in_slot(loc_id) if loc_name not in location_id_to_level_id: continue diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py index ab7fcccdba..545b3c931b 100644 --- a/worlds/smw/Options.py +++ b/worlds/smw/Options.py @@ -1,12 +1,14 @@ from dataclasses import dataclass -from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, PerGameCommonOptions +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionGroup, PerGameCommonOptions class Goal(Choice): """ Determines the goal of the seed + Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser + Yoshi Egg Hunt: Find a certain number of Yoshi Eggs """ display_name = "Goal" @@ -28,7 +30,9 @@ class BossesRequired(Range): class NumberOfYoshiEggs(Range): """ Maximum possible number of Yoshi Eggs that will be in the item pool + If fewer available locations exist in the pool than this number, the number of available locations will be used instead. + Required Percentage of Yoshi Eggs will be calculated based off of that number. """ display_name = "Max Number of Yoshi Eggs" @@ -64,7 +68,9 @@ class MoonChecks(Toggle): class Hidden1UpChecks(Toggle): """ Whether collecting a hidden 1-Up mushroom in a level will grant a check + These checks are considered cryptic as there's no actual indicator that they're in their respective places + Enable this option at your own risk """ display_name = "Hidden 1-Up Checks" @@ -80,7 +86,9 @@ class BonusBlockChecks(Toggle): class Blocksanity(Toggle): """ Whether hitting a block with an item or coin inside will grant a check + Note that some blocks are excluded due to how the option and the game works! + Exclusion list: * Blocks in Top Secret Area & Front Door/Bowser Castle * Blocks that are unreachable unless you glitch your way in @@ -91,10 +99,15 @@ class Blocksanity(Toggle): class BowserCastleDoors(Choice): """ How the doors of Bowser's Castle behave + Vanilla: Front and Back Doors behave as vanilla + Fast: Both doors behave as the Back Door + Slow: Both doors behave as the Front Door + "Front Door" rooms depend on the `bowser_castle_rooms` option + "Back Door" only requires going through the dark hallway to Bowser """ display_name = "Bowser Castle Doors" @@ -107,10 +120,15 @@ class BowserCastleDoors(Choice): class BowserCastleRooms(Choice): """ How the rooms of Bowser's Castle Front Door behave + Vanilla: You can choose which rooms to enter, as in vanilla + Random Two Room: Two random rooms are chosen + Random Five Room: Five random rooms are chosen + Gauntlet: All eight rooms must be cleared + Labyrinth: Which room leads to Bowser? """ display_name = "Bowser Castle Rooms" @@ -125,9 +143,13 @@ class BowserCastleRooms(Choice): class BossShuffle(Choice): """ How bosses are shuffled + None: Bosses are not shuffled + Simple: Four Reznors and the seven Koopalings are shuffled around + Full: Each boss location gets a fully random boss + Singularity: One or two bosses are chosen and placed at every boss location """ display_name = "Boss Shuffle" @@ -148,6 +170,7 @@ class LevelShuffle(Toggle): class ExcludeSpecialZone(Toggle): """ If active, this option will prevent any progression items from being placed in Special Zone levels. + Additionally, if Level Shuffle is active, Special Zone levels will not be shuffled away from their vanilla tiles. """ display_name = "Exclude Special Zone" @@ -155,9 +178,10 @@ class ExcludeSpecialZone(Toggle): class SwapDonutGhostHouseExits(Toggle): """ - If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House - overworld tile go: + If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go: + False: Normal Exit goes up, Secret Exit goes right. + True: Normal Exit goes right, Secret Exit goes up. """ display_name = "Swap Donut GH Exits" @@ -258,6 +282,7 @@ class Autosave(DefaultOnToggle): class EarlyClimb(Toggle): """ Force Climb to appear early in the seed as a local item. + This is particularly useful to prevent BK when Level Shuffle is disabled """ display_name = "Early Climb" @@ -277,9 +302,13 @@ class OverworldSpeed(Choice): class MusicShuffle(Choice): """ Music shuffle type + None: No Music is shuffled + Consistent: Each music track is consistently shuffled throughout the game + Full: Each individual level has a random music track + Singularity: The entire game uses one song for overworld and one song for levels """ display_name = "Music Shuffle" @@ -293,9 +322,13 @@ class MusicShuffle(Choice): class SFXShuffle(Choice): """ Shuffles almost every instance of sound effect playback + Archipelago elements that play sound effects aren't randomized + None: No SFX are shuffled + Full: Each individual SFX call has a random SFX + Singularity: The entire game uses one SFX for every SFX call """ display_name = "Sound Effect Shuffle" @@ -324,8 +357,11 @@ class MarioPalette(Choice): class LevelPaletteShuffle(Choice): """ Whether to shuffle level palettes + Off: Do not shuffle palettes + On Legacy: Uses only the palette sets from the original game + On Curated: Uses custom, hand-crafted palette sets """ display_name = "Level Palette Shuffle" @@ -338,8 +374,11 @@ class LevelPaletteShuffle(Choice): class OverworldPaletteShuffle(Choice): """ Whether to shuffle overworld palettes + Off: Do not shuffle palettes + On Legacy: Uses only the palette sets from the original game + On Curated: Uses custom, hand-crafted palette sets """ display_name = "Overworld Palette Shuffle" @@ -359,6 +398,52 @@ class StartingLifeCount(Range): default = 5 +smw_option_groups = [ + OptionGroup("Goal Options", [ + Goal, + BossesRequired, + NumberOfYoshiEggs, + PercentageOfYoshiEggs, + ]), + OptionGroup("Sanity Options", [ + DragonCoinChecks, + MoonChecks, + Hidden1UpChecks, + BonusBlockChecks, + Blocksanity, + ]), + OptionGroup("Level Shuffling", [ + LevelShuffle, + ExcludeSpecialZone, + BowserCastleDoors, + BowserCastleRooms, + BossShuffle, + SwapDonutGhostHouseExits, + ]), + OptionGroup("Junk and Traps", [ + JunkFillPercentage, + TrapFillPercentage, + IceTrapWeight, + StunTrapWeight, + LiteratureTrapWeight, + TimerTrapWeight, + ReverseTrapWeight, + ThwimpTrapWeight, + ]), + OptionGroup("Aesthetics", [ + DisplayReceivedItemPopups, + Autosave, + OverworldSpeed, + MusicShuffle, + SFXShuffle, + MarioPalette, + LevelPaletteShuffle, + OverworldPaletteShuffle, + StartingLifeCount, + ]), +] + + @dataclass class SMWOptions(PerGameCommonOptions): death_link: DeathLink diff --git a/worlds/smw/Presets.py b/worlds/smw/Presets.py new file mode 100644 index 0000000000..17a80e3efc --- /dev/null +++ b/worlds/smw/Presets.py @@ -0,0 +1,57 @@ +from typing import Dict, Any + +all_random = { + "goal": "random", + "bosses_required": "random", + "max_yoshi_egg_cap": "random", + "percentage_of_yoshi_eggs": "random", + "dragon_coin_checks": "random", + "moon_checks": "random", + "hidden_1up_checks": "random", + "bonus_block_checks": "random", + "blocksanity": "random", + "bowser_castle_doors": "random", + "bowser_castle_rooms": "random", + "level_shuffle": "random", + "exclude_special_zone": "random", + "boss_shuffle": "random", + "swap_donut_gh_exits": "random", + "display_received_item_popups": "random", + "junk_fill_percentage": "random", + "trap_fill_percentage": "random", + "ice_trap_weight": "random", + "stun_trap_weight": "random", + "literature_trap_weight": "random", + "timer_trap_weight": "random", + "reverse_trap_weight": "random", + "thwimp_trap_weight": "random", + "autosave": "random", + "early_climb": "random", + "overworld_speed": "random", + "music_shuffle": "random", + "sfx_shuffle": "random", + "mario_palette": "random", + "level_palette_shuffle": "random", + "overworld_palette_shuffle": "random", + "starting_life_count": "random", +} + +allsanity = { + "dragon_coin_checks": True, + "moon_checks": True, + "hidden_1up_checks": True, + "bonus_block_checks": True, + "blocksanity": True, + "level_shuffle": True, + "boss_shuffle": "full", + "music_shuffle": "full", + "sfx_shuffle": "full", + "mario_palette": "random", + "level_palette_shuffle": "on_curated", + "overworld_palette_shuffle": "on_curated", +} + +smw_options_presets: Dict[str, Dict[str, Any]] = { + "All Random": all_random, + "Allsanity": allsanity, +} diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index 875491a8d0..97fc84f003 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -6,17 +6,19 @@ import settings import threading from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from .Items import SMWItem, ItemData, item_table, junk_table -from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names -from .Options import SMWOptions -from .Regions import create_regions, connect_regions -from .Levels import full_level_list, generate_level_list, location_id_to_level_id -from .Rules import set_rules -from worlds.generic.Rules import add_rule, exclusion_rules -from .Names import ItemName, LocationName -from .Client import SMWSNIClient from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_rule, exclusion_rules + +from .Client import SMWSNIClient +from .Items import SMWItem, ItemData, item_table, junk_table +from .Levels import full_level_list, generate_level_list, location_id_to_level_id +from .Locations import SMWLocation, all_locations, setup_locations, special_zone_level_names, special_zone_dragon_coin_names, special_zone_hidden_1up_names, special_zone_blocksanity_names +from .Names import ItemName, LocationName +from .Options import SMWOptions, smw_option_groups +from .Presets import smw_options_presets +from .Regions import create_regions, connect_regions from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch +from .Rules import set_rules class SMWSettings(settings.Group): @@ -40,9 +42,12 @@ class SMWWeb(WebWorld): "setup/en", ["PoryGone"] ) - + tutorials = [setup_en] + option_groups = smw_option_groups + options_presets = smw_options_presets + class SMWWorld(World): """ diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 0a248aa5d3..3c90ead006 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -109,7 +109,7 @@ class SMZ3SNIClient(SNIClient): location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] + location = ctx.location_names.lookup_in_slot(location_id) snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) @@ -132,8 +132,7 @@ class SMZ3SNIClient(SNIClient): item_out_ptr += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), item_out_ptr, len(ctx.items_received))) await snes_flush_writes(ctx) - diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b030e3fa50..6056a171d3 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -68,7 +68,6 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 061322650e..3baed165d8 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -176,7 +176,6 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 5 web = SoEWebWorld() required_client_version = (0, 4, 4) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index d8a9322ab4..5b0e1e17f2 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -30,7 +30,6 @@ class SpireWorld(World): option_definitions = spire_options game = "Slay the Spire" topology_present = False - data_version = 2 web = SpireWeb() required_client_version = (0, 3, 7) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 6a82a2a26d..61c8666316 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -13,6 +13,7 @@ from .locations import location_table, create_locations, LocationData, locations from .logic.bundle_logic import BundleLogic from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS +from .option_groups import sv_option_groups from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization from .presets import sv_options_presets @@ -39,6 +40,7 @@ class StardewWebWorld(WebWorld): theme = "dirt" bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" options_presets = sv_options_presets + option_groups = sv_option_groups tutorials = [ Tutorial( @@ -71,7 +73,6 @@ class StardewValleyWorld(World): [location.name for location in locations] for group, locations in locations_by_tag.items() } - data_version = 3 required_client_version = (0, 4, 0) options_dataclass = StardewValleyOptions diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index a3096cf789..9ecb2ba364 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -735,26 +735,26 @@ id,name,classification,groups,mod_name 10007,Tractor Garage,useful,,Tractor Mod 10008,Woods Obelisk,progression,,DeepWoods 10009,Spell: Clear Debris,progression,MAGIC_SPELL,Magic -10010,Spell: Till,useful,MAGIC_SPELL,Magic +10010,Spell: Till,progression,MAGIC_SPELL,Magic 10011,Spell: Water,progression,MAGIC_SPELL,Magic 10012,Spell: Blink,progression,MAGIC_SPELL,Magic -10013,Spell: Evac,useful,MAGIC_SPELL,Magic -10014,Spell: Haste,useful,MAGIC_SPELL,Magic +10013,Spell: Evac,progression,MAGIC_SPELL,Magic +10014,Spell: Haste,progression,MAGIC_SPELL,Magic 10015,Spell: Heal,progression,MAGIC_SPELL,Magic -10016,Spell: Buff,useful,MAGIC_SPELL,Magic +10016,Spell: Buff,progression,MAGIC_SPELL,Magic 10017,Spell: Shockwave,progression,MAGIC_SPELL,Magic 10018,Spell: Fireball,progression,MAGIC_SPELL,Magic 10019,Spell: Frostbolt,progression,MAGIC_SPELL,Magic 10020,Spell: Teleport,progression,MAGIC_SPELL,Magic -10021,Spell: Lantern,useful,MAGIC_SPELL,Magic +10021,Spell: Lantern,progression,MAGIC_SPELL,Magic 10022,Spell: Tendrils,progression,MAGIC_SPELL,Magic -10023,Spell: Photosynthesis,useful,MAGIC_SPELL,Magic +10023,Spell: Photosynthesis,progression,MAGIC_SPELL,Magic 10024,Spell: Descend,progression,MAGIC_SPELL,Magic 10025,Spell: Meteor,progression,MAGIC_SPELL,Magic -10026,Spell: Bloodmana,useful,MAGIC_SPELL,Magic -10027,Spell: Lucksteal,useful,MAGIC_SPELL,Magic +10026,Spell: Bloodmana,progression,MAGIC_SPELL,Magic +10027,Spell: Lucksteal,progression,MAGIC_SPELL,Magic 10028,Spell: Spirit,progression,MAGIC_SPELL,Magic -10029,Spell: Rewind,useful,MAGIC_SPELL,Magic +10029,Spell: Rewind,progression,MAGIC_SPELL,Magic 10030,Pendant of Community,progression,,DeepWoods 10031,Pendant of Elders,progression,,DeepWoods 10032,Pendant of Depths,progression,,DeepWoods diff --git a/worlds/stardew_valley/mods/logic/magic_logic.py b/worlds/stardew_valley/mods/logic/magic_logic.py index 99482b0630..662ff3acae 100644 --- a/worlds/stardew_valley/mods/logic/magic_logic.py +++ b/worlds/stardew_valley/mods/logic/magic_logic.py @@ -8,7 +8,7 @@ from ...mods.mod_data import ModNames from ...stardew_rule import StardewRule, False_ from ...strings.ap_names.skill_level_names import ModSkillLevel from ...strings.region_names import MagicRegion -from ...strings.spells import MagicSpell +from ...strings.spells import MagicSpell, all_spells class MagicLogicMixin(BaseLogicMixin): @@ -27,7 +27,8 @@ class MagicLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, HasLogicM def can_use_altar(self) -> StardewRule: if ModNames.magic not in self.options.mods: return False_() - return self.logic.region.can_reach(MagicRegion.altar) + spell_rule = False_() + return self.logic.region.can_reach(MagicRegion.altar) & self.logic.received_any(*all_spells) def has_any_spell(self) -> StardewRule: if ModNames.magic not in self.options.mods: diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py new file mode 100644 index 0000000000..50709c10fd --- /dev/null +++ b/worlds/stardew_valley/option_groups.py @@ -0,0 +1,65 @@ +from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility +from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, + EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, + ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, + FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, + QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, + NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems, + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, + FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, + Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods) + +sv_option_groups = [ + OptionGroup("General", [ + Goal, + FarmType, + BundleRandomization, + BundlePrice, + EntranceRandomization, + ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + SeasonRandomization, + Cropsanity, + BackpackProgression, + ToolProgression, + ElevatorProgression, + SkillProgression, + BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + FestivalLocations, + ArcadeMachineLocations, + SpecialOrderLocations, + QuestLocations, + Fishsanity, + Museumsanity, + Friendsanity, + FriendsanityHeartSize, + Monstersanity, + Shipsanity, + Cooksanity, + Chefsanity, + Craftsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + StartingMoney, + ProfitMargin, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + NumberOfMovementBuffs, + NumberOfLuckBuffs, + TrapItems, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + QuickStart, + ]), + OptionGroup("Advanced Options", [ + Gifting, + DeathLink, + Mods, + ProgressionBalancing, + Accessibility, + ]), +] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 191a634496..ba1ebfb9c1 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -697,8 +697,6 @@ class Mods(OptionSet): class StardewValleyOptions(PerGameCommonOptions): goal: Goal farm_type: FarmType - starting_money: StartingMoney - profit_margin: ProfitMargin bundle_randomization: BundleRandomization bundle_price: BundlePrice entrance_randomization: EntranceRandomization @@ -722,16 +720,18 @@ class StardewValleyOptions(PerGameCommonOptions): craftsanity: Craftsanity friendsanity: Friendsanity friendsanity_heart_size: FriendsanityHeartSize - movement_buff_number: NumberOfMovementBuffs - luck_buff_number: NumberOfLuckBuffs exclude_ginger_island: ExcludeGingerIsland - trap_items: TrapItems - multiple_day_sleep_enabled: MultipleDaySleepEnabled - multiple_day_sleep_cost: MultipleDaySleepCost + quick_start: QuickStart + starting_money: StartingMoney + profit_margin: ProfitMargin experience_multiplier: ExperienceMultiplier friendship_multiplier: FriendshipMultiplier debris_multiplier: DebrisMultiplier - quick_start: QuickStart + movement_buff_number: NumberOfMovementBuffs + luck_buff_number: NumberOfLuckBuffs + trap_items: TrapItems + multiple_day_sleep_enabled: MultipleDaySleepEnabled + multiple_day_sleep_cost: MultipleDaySleepCost gifting: Gifting mods: Mods death_link: DeathLink diff --git a/worlds/stardew_valley/strings/spells.py b/worlds/stardew_valley/strings/spells.py index ef5545c569..4b246c173a 100644 --- a/worlds/stardew_valley/strings/spells.py +++ b/worlds/stardew_valley/strings/spells.py @@ -1,22 +1,30 @@ +all_spells = [] + + +def spell(name: str) -> str: + all_spells.append(name) + return name + + class MagicSpell: - clear_debris = "Spell: Clear Debris" - till = "Spell: Till" - water = "Spell: Water" - blink = "Spell: Blink" - evac = "Spell: Evac" - haste = "Spell: Haste" - heal = "Spell: Heal" - buff = "Spell: Buff" - shockwave = "Spell: Shockwave" - fireball = "Spell: Fireball" - frostbite = "Spell: Frostbolt" - teleport = "Spell: Teleport" - lantern = "Spell: Lantern" - tendrils = "Spell: Tendrils" - photosynthesis = "Spell: Photosynthesis" - descend = "Spell: Descend" - meteor = "Spell: Meteor" - bloodmana = "Spell: Bloodmana" - lucksteal = "Spell: Lucksteal" - spirit = "Spell: Spirit" - rewind = "Spell: Rewind" + clear_debris = spell("Spell: Clear Debris") + till = spell("Spell: Till") + water = spell("Spell: Water") + blink = spell("Spell: Blink") + evac = spell("Spell: Evac") + haste = spell("Spell: Haste") + heal = spell("Spell: Heal") + buff = spell("Spell: Buff") + shockwave = spell("Spell: Shockwave") + fireball = spell("Spell: Fireball") + frostbite = spell("Spell: Frostbolt") + teleport = spell("Spell: Teleport") + lantern = spell("Spell: Lantern") + tendrils = spell("Spell: Tendrils") + photosynthesis = spell("Spell: Photosynthesis") + descend = spell("Spell: Descend") + meteor = spell("Spell: Meteor") + bloodmana = spell("Spell: Bloodmana") + lucksteal = spell("Spell: Lucksteal") + spirit = spell("Spell: Spirit") + rewind = spell("Spell: Rewind") diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 08df70d78b..856117469e 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,6 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - data_version = 10 required_client_version = (0, 4, 1) creatures_to_scan: List[str] diff --git a/worlds/terraria/Options.py b/worlds/terraria/Options.py index 1f9ba69afe..4c4b96056c 100644 --- a/worlds/terraria/Options.py +++ b/worlds/terraria/Options.py @@ -1,5 +1,5 @@ -from Options import Choice, Option, Toggle, DeathLink -import typing +from dataclasses import dataclass +from Options import Choice, DeathLink, PerGameCommonOptions class Goal(Choice): @@ -49,9 +49,9 @@ class FillExtraChecksWith(Choice): default = 1 -options: typing.Dict[str, type(Option)] = { # type: ignore - "goal": Goal, - "achievements": Achievements, - "fill_extra_checks_with": FillExtraChecksWith, - "death_link": DeathLink, -} +@dataclass +class TerrariaOptions(PerGameCommonOptions): + goal: Goal + achievements: Achievements + fill_extra_checks_with: FillExtraChecksWith + death_link: DeathLink diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py index 6ef281157f..abc10a7bb3 100644 --- a/worlds/terraria/__init__.py +++ b/worlds/terraria/__init__.py @@ -25,7 +25,7 @@ from .Checks import ( armor_minions, accessory_minions, ) -from .Options import options +from .Options import TerrariaOptions class TerrariaWeb(WebWorld): @@ -49,12 +49,8 @@ class TerrariaWorld(World): game = "Terraria" web = TerrariaWeb() - option_definitions = options - - # data_version is used to signal that items, locations or their names - # changed. Set this to 0 during development so other games' clients do not - # cache any texts, then increase by 1 for each release that makes changes. - data_version = 2 + options_dataclass = TerrariaOptions + options: TerrariaOptions item_name_to_id = item_name_to_id location_name_to_id = location_name_to_id @@ -70,7 +66,7 @@ class TerrariaWorld(World): goal_locations: Set[str] def generate_early(self) -> None: - goal, goal_locations = goals[self.multiworld.goal[self.player].value] + goal, goal_locations = goals[self.options.goal.value] ter_goals = {} goal_items = set() for location in goal_locations: @@ -79,7 +75,7 @@ class TerrariaWorld(World): ter_goals[item] = location goal_items.add(item) - achievements = self.multiworld.achievements[self.player].value + achievements = self.options.achievements.value location_count = 0 locations = [] for rule, flags, _, _ in rules[:goal]: @@ -89,7 +85,7 @@ class TerrariaWorld(World): or (achievements < 2 and "Grindy" in flags) or (achievements < 3 and "Fishing" in flags) or ( - rule == "Zenith" and self.multiworld.goal[self.player].value != 11 + rule == "Zenith" and self.options.goal.value != 11 ) # Bad hardcoding ): continue @@ -123,7 +119,7 @@ class TerrariaWorld(World): # Event items.append(rule) - extra_checks = self.multiworld.fill_extra_checks_with[self.player].value + extra_checks = self.options.fill_extra_checks_with.value ordered_rewards = [ reward for reward in labels["ordered"] @@ -241,7 +237,7 @@ class TerrariaWorld(World): elif condition == "calamity": return sign == self.calamity elif condition == "grindy": - return sign == (self.multiworld.achievements[self.player].value >= 2) + return sign == (self.options.achievements.value >= 2) elif condition == "pickaxe": if type(arg) is not int: raise Exception("@pickaxe requires an integer argument") @@ -340,6 +336,6 @@ class TerrariaWorld(World): def fill_slot_data(self) -> Dict[str, object]: return { "goal": list(self.goal_locations), - "achievements": self.multiworld.achievements[self.player].value, - "deathlink": bool(self.multiworld.death_link[self.player]), + "achievements": self.options.achievements.value, + "deathlink": bool(self.options.death_link), } diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index ff7b3515e6..cab6fb648b 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,7 +39,6 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 12 web = TimespinnerWebWorld() required_client_version = (0, 4, 2) @@ -228,7 +227,7 @@ class TimespinnerWorld(World): non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 7565dc0147..a1f9081418 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -68,7 +68,6 @@ class TLoZWorld(World): settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False - data_version = 1 base_id = 7000 web = TLoZWeb() diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 8f28a67a1b..f3dfee6404 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -8,9 +8,9 @@ from .er_rules import set_er_location_rules from .regions import tunic_regions from .er_scripts import create_er_regions from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando +from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets from worlds.AutoWorld import WebWorld, World -from worlds.generic import PlandoConnection +from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -27,6 +27,8 @@ class TunicWeb(WebWorld): ] theme = "grassFlowers" game = "TUNIC" + option_groups = tunic_option_groups + options_presets = tunic_option_presets class TunicItem(Item): @@ -68,17 +70,17 @@ class TunicWorld(World): seed_groups: Dict[str, SeedGroup] = {} def generate_early(self) -> None: - if self.multiworld.plando_connections[self.player]: - for index, cxn in enumerate(self.multiworld.plando_connections[self.player]): + if self.options.plando_connections: + for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later if cxn.entrance.startswith("Shop"): replacement = PlandoConnection(cxn.exit, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) elif cxn.exit.startswith("Shop"): replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) # Universal tracker stuff, shouldn't do anything in standard gen if hasattr(self.multiworld, "re_gen_passthrough"): diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md index 94a8a03841..58cc1bcf25 100644 --- a/worlds/tunic/docs/setup_en.md +++ b/worlds/tunic/docs/setup_en.md @@ -31,6 +31,8 @@ Download [BepInEx](https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-p If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html). +If playing on Linux, you may be able to add `WINEDLLOVERRIDES="winhttp=n,b" %command%` to your Steam launch options. If this does not work, follow the guide for Steam Deck above. + Extract the contents of the BepInEx .zip file into your TUNIC game directory:
- **Steam**: Steam\steamapps\common\TUNIC
- **PC Game Pass**: XboxGames\Tunic\Content
diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 34ad752be0..b2adc9158a 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -3,8 +3,8 @@ from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd from .er_rules import set_er_region_rules +from Options import PlandoConnection from .options import EntranceRando -from worlds.generic import PlandoConnection from random import Random from copy import deepcopy @@ -194,7 +194,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if world.options.entrance_rando.value in EntranceRando.options: - plando_connections = world.multiworld.plando_connections[world.player] + plando_connections = world.options.plando_connections.value else: plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"] diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 605bb065fd..b3b6b3b96f 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,30 +1,39 @@ from dataclasses import dataclass - -from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions +from typing import Dict, Any +from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, + PerGameCommonOptions, OptionGroup) +from .er_data import portal_mapping class SwordProgression(DefaultOnToggle): - """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.""" + """ + Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power. + """ internal_name = "sword_progression" display_name = "Sword Progression" class StartWithSword(Toggle): - """Start with a sword in the player's inventory. Does not count towards Sword Progression.""" + """ + Start with a sword in the player's inventory. Does not count towards Sword Progression. + """ internal_name = "start_with_sword" display_name = "Start With Sword" class KeysBehindBosses(Toggle): - """Places the three hexagon keys behind their respective boss fight in your world.""" + """ + Places the three hexagon keys behind their respective boss fight in your world. + """ internal_name = "keys_behind_bosses" display_name = "Keys Behind Bosses" class AbilityShuffling(Toggle): - """Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found. + """ + Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found. If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount. - *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes. + * Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes. """ internal_name = "ability_shuffling" display_name = "Shuffle Abilities" @@ -37,9 +46,9 @@ class LogicRules(Choice): No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer. Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. - *Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. - *Using Ladder Storage to get to individual chests is not in logic to avoid tedium. - *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. + * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. + * Using Ladder Storage to get to individual chests is not in logic to avoid tedium. + * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. """ internal_name = "logic_rules" display_name = "Logic Rules" @@ -52,21 +61,27 @@ class LogicRules(Choice): class Lanternless(Toggle): - """Choose whether you require the Lantern for dark areas. - When enabled, the Lantern is marked as Useful instead of Progression.""" + """ + Choose whether you require the Lantern for dark areas. + When enabled, the Lantern is marked as Useful instead of Progression. + """ internal_name = "lanternless" display_name = "Lanternless" class Maskless(Toggle): - """Choose whether you require the Scavenger's Mask for Lower Quarry. - When enabled, the Scavenger's Mask is marked as Useful instead of Progression.""" + """ + Choose whether you require the Scavenger's Mask for Lower Quarry. + When enabled, the Scavenger's Mask is marked as Useful instead of Progression. + """ internal_name = "maskless" display_name = "Maskless" class FoolTraps(Choice): - """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.""" + """ + Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player. + """ internal_name = "fool_traps" display_name = "Fool Traps" option_off = 0 @@ -77,13 +92,17 @@ class FoolTraps(Choice): class HexagonQuest(Toggle): - """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.""" + """ + An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them. + """ internal_name = "hexagon_quest" display_name = "Hexagon Quest" class HexagonGoal(Range): - """How many Gold Questagons are required to complete the game on Hexagon Quest.""" + """ + How many Gold Questagons are required to complete the game on Hexagon Quest. + """ internal_name = "hexagon_goal" display_name = "Gold Hexagons Required" range_start = 15 @@ -92,7 +111,9 @@ class HexagonGoal(Range): class ExtraHexagonPercentage(Range): - """How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.""" + """ + How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount. + """ internal_name = "extra_hexagon_percentage" display_name = "Percentage of Extra Gold Hexagons" range_start = 0 @@ -118,16 +139,20 @@ class EntranceRando(TextChoice): class FixedShop(Toggle): - """Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool. + """ + Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool. Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances. - Has no effect if Entrance Rando is not enabled.""" + Has no effect if Entrance Rando is not enabled. + """ internal_name = "fixed_shop" display_name = "Fewer Shops in Entrance Rando" class LaurelsLocation(Choice): - """Force the Hero's Laurels to be placed at a location in your world. - For if you want to avoid or specify early or late Laurels.""" + """ + Force the Hero's Laurels to be placed at a location in your world. + For if you want to avoid or specify early or late Laurels. + """ internal_name = "laurels_location" display_name = "Laurels Location" option_anywhere = 0 @@ -138,11 +163,21 @@ class LaurelsLocation(Choice): class ShuffleLadders(Toggle): - """Turns several ladders in the game into items that must be found before they can be climbed on. + """ + Turns several ladders in the game into items that must be found before they can be climbed on. Adds more layers of progression to the game by blocking access to many areas early on. - "Ladders were a mistake." —Andrew Shouldice""" + "Ladders were a mistake." + —Andrew Shouldice + """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" + + +class TUNICPlandoConnections(PlandoConnections): + entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + + duplicate_exits = True @dataclass @@ -163,3 +198,34 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation + plando_connections: TUNICPlandoConnections + + +tunic_option_groups = [ + OptionGroup("Logic Options", [ + LogicRules, + Lanternless, + Maskless, + ]) +] + +tunic_option_presets: Dict[str, Dict[str, Any]] = { + "Sync": { + "ability_shuffling": True, + }, + "Async": { + "progression_balancing": 0, + "ability_shuffling": True, + "shuffle_ladders": True, + "laurels_location": "10_fairies", + }, + "Glace Mode": { + "accessibility": "minimal", + "ability_shuffling": True, + "entrance_rando": "yes", + "fool_traps": "onslaught", + "logic_rules": "unrestricted", + "maskless": True, + "lanternless": True, + }, +} diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 0694456a6b..b87d3ac01e 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -52,8 +52,6 @@ class UndertaleWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - data_version = 7 - def _get_undertale_data(self): return { "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 30a76f82cc..3d3ee8cf58 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -34,8 +34,6 @@ class V6World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 1 - area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index f47ab57d5e..ecab25db3d 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -16,7 +16,7 @@ from .data.item_definition_classes import DoorItemDefinition, ItemData from .data.utils import get_audio_logs from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints from .locations import WitnessPlayerLocations, static_witness_locations -from .options import TheWitnessOptions +from .options import TheWitnessOptions, witness_option_groups from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic from .presets import witness_option_presets @@ -36,6 +36,7 @@ class WitnessWebWorld(WebWorld): )] options_presets = witness_option_presets + option_groups = witness_option_groups class WitnessWorld(World): diff --git a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt index 6f9c80fc0a..95c1fc39fb 100644 --- a/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/data/settings/EP_Shuffle/EP_Easy.txt @@ -15,3 +15,4 @@ Disabled Locations: 0x09D63 (Mountain Pink Bridge EP) 0x09D5E (Mountain Blue Bridge EP) 0x09D5D (Mountain Yellow Bridge EP) +0x220BD (Both Orange Bridges EP) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 63f98faea4..f51d86ba22 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from schema import And, Schema -from Options import Choice, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition @@ -61,9 +61,9 @@ class ShuffleLasers(Choice): class ShuffleDoors(Choice): """ If on, opening doors, moving bridges etc. will require a "key". - If set to "panels", the panel on the door will be locked until receiving its corresponding key. - If set to "doors", the door will open immediately upon receiving its key. Door panels are added as location checks. - "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels". + - Panels: The panel on the door will be locked until receiving its corresponding key. + - Doors: The door will open immediately upon receiving its key. Door panels are added as location checks. + - Mixed: Includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels". """ display_name = "Shuffle Doors" option_off = 0 @@ -74,8 +74,10 @@ class ShuffleDoors(Choice): class DoorGroupings(Choice): """ - If set to "none", there will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool. - If set to "regional", all doors in the same general region will open at once with a single key, reducing the amount of door items and complexity. + Controls how door items are grouped. + + - Off: There will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool. + - Regional: All doors in the same general region will open at once with a single key, reducing the amount of door items and complexity. """ display_name = "Door Groupings" option_off = 0 @@ -108,8 +110,8 @@ class ShuffleVaultBoxes(Toggle): class ShuffleEnvironmentalPuzzles(Choice): """ Adds Environmental/Obelisk Puzzles into the location pool. - If set to "individual", every Environmental Puzzle sends an item. - If set to "Obelisk Sides", completing every puzzle on one side of an Obelisk sends an item. + - Individual: Every Environmental Puzzle sends an item. + - Obelisk Sides: Completing every puzzle on one side of an Obelisk sends an item. Note: In Obelisk Sides, any EPs excluded through another option will be pre-completed on their Obelisk. """ @@ -129,9 +131,9 @@ class ShuffleDog(Toggle): class EnvironmentalPuzzlesDifficulty(Choice): """ When "Shuffle Environmental Puzzles" is on, this setting governs which EPs are eligible for the location pool. - If set to "eclipse", every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP". - If set to "tedious", Theater Eclipse EP is excluded from the location pool. - If set to "normal", several other difficult or long EPs are excluded as well. + - Eclipse: Every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP". + - Tedious Theater Eclipse EP is excluded from the location pool. + - Normal: several other difficult or long EPs are excluded as well. """ display_name = "Environmental Puzzles Difficulty" option_normal = 0 @@ -159,10 +161,10 @@ class ShufflePostgame(Toggle): class VictoryCondition(Choice): """ Set the victory condition for this world. - Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers). - Challenge: Beat the secret Challenge (requires Challenge Lasers). - Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers). - Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers). + - Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers). + - Challenge: Beat the secret Challenge (requires Challenge Lasers). + - Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers). + - Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers). It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser to count, the laser locks on the Elevator and Challenge Timer panels do not. @@ -332,3 +334,45 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + + +witness_option_groups = [ + OptionGroup("Puzzles & Goal", [ + PuzzleRandomization, + VictoryCondition, + MountainLasers, + ChallengeLasers, + ]), + OptionGroup("Locations", [ + ShuffleDiscardedPanels, + ShuffleVaultBoxes, + ShuffleEnvironmentalPuzzles, + EnvironmentalPuzzlesDifficulty, + ShufflePostgame, + DisableNonRandomizedPuzzles, + ]), + OptionGroup("Progression Items", [ + ShuffleSymbols, + ShuffleDoors, + DoorGroupings, + ShuffleLasers, + ShuffleBoat, + ObeliskKeys, + ]), + OptionGroup("Filler Items", [ + PuzzleSkipAmount, + TrapPercentage, + TrapWeights + ]), + OptionGroup("Hints", [ + HintAmount, + AreaHintPercentage, + LaserHints + ]), + OptionGroup("Misc", [ + EarlyCaves, + ElevatorsComeToYou, + DeathLink, + DeathLinkAmnesty, + ]) +] diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index 1aff36c553..2a710b046a 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -116,7 +116,7 @@ class YoshisIslandSNIClient(SNIClient): for new_check_id in new_checks: ctx.locations_checked.add(new_check_id) - location = ctx.location_names[new_check_id] + location = ctx.location_names.lookup_in_slot(new_check_id) total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) @@ -127,9 +127,9 @@ class YoshisIslandSNIClient(SNIClient): item = ctx.items_received[recv_index] recv_index += 1 logging.info("Received %s from %s (%s) (%d/%d in list)" % ( - color(ctx.item_names[item.item], "red", "bold"), + color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"), color(ctx.player_names[item.player], "yellow"), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) + ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received))) snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) if item.item in item_values: diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 2640b13aca..1cf44f090f 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -399,12 +399,14 @@ class Yugioh06World(World): self.playerName.extend([0] * (0x20 - len(self.playerName))) patch = YGO06ProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "patch.bsdiff4")) + procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] if self.is_draft_mode: - patch.procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["draft_patch.bsdiff4"])) patch.write_file("draft_patch.bsdiff4", pkgutil.get_data(__name__, "patches/draft.bsdiff4")) if self.options.ocg_arts: - patch.procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) + procedure.insert(1, ("apply_bsdiff4", ["ocg_patch.bsdiff4"])) patch.write_file("ocg_patch.bsdiff4", pkgutil.get_data(__name__, "patches/ocg.bsdiff4")) + patch.procedure = procedure write_tokens(self, patch) # Write Output diff --git a/worlds/yugioh06/rom.py b/worlds/yugioh06/rom.py index 0bd3f1cb76..3ac10f9ea4 100644 --- a/worlds/yugioh06/rom.py +++ b/worlds/yugioh06/rom.py @@ -22,8 +22,6 @@ class YGO06ProcedurePatch(APProcedurePatch, APTokenMixin): patch_file_ending = ".apygo06" result_file_ending = ".gba" - procedure = [("apply_bsdiff4", ["base_patch.bsdiff4"]), ("apply_tokens", ["token_data.bin"])] - @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py index 53ea95b27b..a804c7e728 100644 --- a/worlds/yugioh06/rules.py +++ b/worlds/yugioh06/rules.py @@ -154,11 +154,11 @@ def set_rules(world): lambda state: state.has_all(["Yata-Garasu", "Chaos Emperor Dragon - Envoy of the End", "Sangan"], player) and state.has_any(["No Banlist", "Banlist September 2003"], player), "Can Stall with Monsters": - lambda state: state.count_from_list_exclusive( + lambda state: state.count_from_list_unique( ["Spirit Reaper", "Giant Germ", "Marshmallon", "Nimble Momonga"], player) >= 2, "Can Stall with ST": - lambda state: state.count_from_list_exclusive(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"], - player) >= 2, + lambda state: state.count_from_list_unique(["Level Limit - Area B", "Gravity Bind", "Messenger of Peace"], + player) >= 2, "Has Back-row removal": lambda state: back_row_removal(state, player) @@ -201,8 +201,8 @@ def set_rules(world): lambda state: yugioh06_difficulty(state, player, 3), "LD18 Attacks forbidden": lambda state: state.has_all(["Wave-Motion Cannon", "Stealth Bird"], player) - and state.count_from_list_exclusive(["Dark World Lightning", "Nobleman of Crossout", - "Shield Crash", "Tribute to the Doomed"], player) >= 2 + and state.count_from_list_unique(["Dark World Lightning", "Nobleman of Crossout", + "Shield Crash", "Tribute to the Doomed"], player) >= 2 and yugioh06_difficulty(state, player, 3), "LD19 All except E-Hero's forbidden": lambda state: state.has_any(["Polymerization", "Fusion Gate"], player) and @@ -363,7 +363,7 @@ def set_rules(world): "TD30 Tribute Summon": lambda state: state.has("Treeborn Frog", player) and yugioh06_difficulty(state, player, 2), "TD31 Special Summon C": - lambda state: state.count_from_list_exclusive( + lambda state: state.count_from_list_unique( ["Aqua Spirit", "Rock Spirit", "Spirit of Flames", "Garuda the Wind Spirit", "Gigantes", "Inferno", "Megarock Dragon", "Silpheed"], player) > 4 and yugioh06_difficulty(state, player, 3), @@ -393,11 +393,11 @@ def set_rules(world): and yugioh06_difficulty(state, player, 3), "TD39 Raviel, Lord of Phantasms": lambda state: state.has_all(["Raviel, Lord of Phantasms", "Giant Germ"], player) and - state.count_from_list_exclusive(["Archfiend Soldier", - "Skull Descovery Knight", - "Slate Warrior", - "D. D. Trainer", - "Earthbound Spirit"], player) >= 3 + state.count_from_list_unique(["Archfiend Soldier", + "Skull Descovery Knight", + "Slate Warrior", + "D. D. Trainer", + "Earthbound Spirit"], player) >= 3 and yugioh06_difficulty(state, player, 3), "TD40 Make a Chain": lambda state: state.has("Ultimate Offering", player) @@ -450,20 +450,20 @@ def set_rules(world): def only_light(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Dunames Dark Witch", "X-Head Cannon", "Homunculus the Alchemic Being", "Hysteric Fairy", "Ninja Grandmaster Sasuke"], player, 2)\ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Chaos Command Magician", "Cybernetic Magician", "Kaiser Glider", "The Agent of Judgment - Saturn", "Zaborg the Thunder Monarch", "Cyber Dragon"], player, 1) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "D.D. Warrior Lady", "Mystic Swordsman LV2", "Y-Dragon Head", @@ -472,7 +472,7 @@ def only_light(state, player): def only_dark(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Dark Elf", "Archfiend Soldier", "Mad Dog of Darkness", @@ -501,7 +501,7 @@ def only_dark(state, player): "Jinzo", "Ryu Kokki" ], player) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Legendary Fiend", "Don Zaloog", "Newdoria", @@ -512,7 +512,7 @@ def only_dark(state, player): def only_earth(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Berserk Gorilla", "Gemini Elf", "Insect Knight", @@ -527,7 +527,7 @@ def only_earth(state, player): "Granmarg the Rock Monarch", "Hieracosphinx", "Saber Beetle" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Hyper Hammerhead", "Green Gadget", "Red Gadget", @@ -539,7 +539,7 @@ def only_earth(state, player): def only_water(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Gagagigo", "Familiar-Possessed - Eria", "7 Colored Fish", @@ -550,7 +550,7 @@ def only_water(state, player): "Amphibian Beast", "Terrorking Salmon", "Mobius the Frost Monarch" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Revival Jam", "Yomi Ship", "Treeborn Frog" @@ -558,7 +558,7 @@ def only_water(state, player): def only_fire(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Blazing Inpachi", "Familiar-Possessed - Hiita", "Great Angus", @@ -566,7 +566,7 @@ def only_fire(state, player): ], player, 2) and state.has_any([ "Thestalos the Firestorm Monarch", "Horus the Black Flame Dragon LV6" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Solar Flare Dragon", "Tenkabito Shien", "Ultimate Baseball Kid" @@ -574,7 +574,7 @@ def only_fire(state, player): def only_wind(state, player): - return state.has_from_list_exclusive([ + return state.has_from_list_unique([ "Luster Dragon", "Slate Warrior", "Spear Dragon", @@ -588,7 +588,7 @@ def only_wind(state, player): "Luster Dragon #2", "Armed Dragon LV5", "Roc from the Valley of Haze" - ], player) and state.has_from_list_exclusive([ + ], player) and state.has_from_list_unique([ "Armed Dragon LV3", "Twin-Headed Behemoth", "Harpie Lady 1" @@ -599,7 +599,7 @@ def only_fairy(state, player): return state.has_any([ "Dunames Dark Witch", "Hysteric Fairy" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Dunames Dark Witch", "Hysteric Fairy", "Dancing Fairy", @@ -623,7 +623,7 @@ def only_warrior(state, player): "Gearfried the Iron knight", "Ninja Grandmaster Sasuke", "Warrior Beaters" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Warrior Lady of the Wasteland", "Exiled Force", "Mystic Swordsman LV2", @@ -644,7 +644,7 @@ def only_warrior(state, player): def only_zombie(state, player): return state.has("Pyramid Turtle", player) \ - and state.has_from_list_exclusive([ + and state.has_from_list_unique([ "Regenerating Mummy", "Ryu Kokki", "Spirit Reaper", @@ -665,7 +665,7 @@ def only_dragon(state, player): "Luster Dragon", "Spear Dragon", "Cave Dragon" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Luster Dragon", "Spear Dragon", "Cave Dragon" @@ -692,7 +692,7 @@ def only_spellcaster(state, player): "Toon Gemini Elf", "Kycoo the Ghost Destroyer", "Familiar-Possessed - Aussa" - ], player) and (state.count_from_list_exclusive([ + ], player) and (state.count_from_list_unique([ "Dark Elf", "Gemini Elf", "Skilled Dark Magician", @@ -730,7 +730,7 @@ def equip_unions(state, player): def can_gain_lp_every_turn(state, player): - return state.count_from_list_exclusive([ + return state.count_from_list_unique([ "Solemn Wishes", "Cure Mermaid", "Dancing Fairy", @@ -739,7 +739,7 @@ def can_gain_lp_every_turn(state, player): def only_normal(state, player): - return (state.has_from_list_exclusive([ + return (state.has_from_list_unique([ "Archfiend Soldier", "Gemini Elf", "Insect Knight", @@ -784,21 +784,21 @@ def only_level(state, player): def spell_counter(state, player): return (state.has("Pitch-Black Power Stone", player) and - state.has_from_list_exclusive(["Blast Magician", - "Magical Marionette", - "Mythical Beast Cerberus", - "Royal Magical Library", - "Spell-Counter Cards"], player, 2)) + state.has_from_list_unique(["Blast Magician", + "Magical Marionette", + "Mythical Beast Cerberus", + "Royal Magical Library", + "Spell-Counter Cards"], player, 2)) def take_control(state, player): - return state.has_from_list_exclusive(["Aussa the Earth Charmer", - "Jowls of Dark Demise", - "Brain Control", - "Creature Swap", - "Enemy Controller", - "Mind Control", - "Magician of Faith"], player, 5) + return state.has_from_list_unique(["Aussa the Earth Charmer", + "Jowls of Dark Demise", + "Brain Control", + "Creature Swap", + "Enemy Controller", + "Mind Control", + "Magician of Faith"], player, 5) def only_toons(state, player): @@ -818,51 +818,51 @@ def only_spirit(state, player): def pacman_deck(state, player): - return state.has_from_list_exclusive(["Des Lacooda", - "Swarm of Locusts", - "Swarm of Scarabs", - "Wandering Mummy", - "Golem Sentry", - "Great Spirit", - "Royal Keeper", - "Stealth Bird"], player, 4) + return state.has_from_list_unique(["Des Lacooda", + "Swarm of Locusts", + "Swarm of Scarabs", + "Wandering Mummy", + "Golem Sentry", + "Great Spirit", + "Royal Keeper", + "Stealth Bird"], player, 4) def quick_plays(state, player): - return state.has_from_list_exclusive(["Collapse", - "Emergency Provisions", - "Enemy Controller", - "Graceful Dice", - "Mystik Wok", - "Offerings to the Doomed", - "Poison of the Old Man", - "Reload", - "Rush Recklessly", - "The Reliable Guardian"], player, 4) + return state.has_from_list_unique(["Collapse", + "Emergency Provisions", + "Enemy Controller", + "Graceful Dice", + "Mystik Wok", + "Offerings to the Doomed", + "Poison of the Old Man", + "Reload", + "Rush Recklessly", + "The Reliable Guardian"], player, 4) def counter_traps(state, player): - return state.has_from_list_exclusive(["Cursed Seal of the Forbidden Spell", - "Divine Wrath", - "Horn of Heaven", - "Magic Drain", - "Magic Jammer", - "Negate Attack", - "Seven Tools of the Bandit", - "Solemn Judgment", - "Spell Shield Type-8"], player, 5) + return state.has_from_list_unique(["Cursed Seal of the Forbidden Spell", + "Divine Wrath", + "Horn of Heaven", + "Magic Drain", + "Magic Jammer", + "Negate Attack", + "Seven Tools of the Bandit", + "Solemn Judgment", + "Spell Shield Type-8"], player, 5) def back_row_removal(state, player): - return state.has_from_list_exclusive(["Anteatereatingant", - "B.E.S. Tetran", - "Breaker the Magical Warrior", - "Calamity of the Wicked", - "Chiron the Mage", - "Dust Tornado", - "Heavy Storm", - "Mystical Space Typhoon", - "Mobius the Frost Monarch", - "Raigeki Break", - "Stamping Destruction", - "Swarm of Locusts"], player, 2) + return state.has_from_list_unique(["Anteatereatingant", + "B.E.S. Tetran", + "Breaker the Magical Warrior", + "Calamity of the Wicked", + "Chiron the Mage", + "Dust Tornado", + "Heavy Storm", + "Mystical Space Typhoon", + "Mobius the Frost Monarch", + "Raigeki Break", + "Stamping Destruction", + "Swarm of Locusts"], player, 2) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cce120d7e3..205cc9ad6b 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -86,11 +86,6 @@ class ZillionWorld(World): item_name_to_id = _item_name_to_id location_name_to_id = _loc_name_to_id - # increment this every time something in your world's names/id mappings changes. - # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be - # retrieved by clients on every connection. - data_version = 1 - logger: logging.Logger class LogStreamInterface: