diff --git a/Generate.py b/Generate.py index 8c649d76b770..1b36c633d8ec 100644 --- a/Generate.py +++ b/Generate.py @@ -120,7 +120,7 @@ def main(args=None, callback=ERmain): raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e # sort dict for consistent results across platforms: - weights_cache = {key: value for key, value in sorted(weights_cache.items())} + weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())} for filename, yaml_data in weights_cache.items(): if filename not in {args.meta_file_path, args.weights_file_path}: for yaml in yaml_data: @@ -353,7 +353,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) return category_dict[option_key] - raise Exception(f"Error generating meta option {option_key} for {game}.") + raise Options.OptionError(f"Error generating meta option {option_key} for {game}.") def roll_linked_options(weights: dict) -> dict: @@ -409,19 +409,19 @@ def roll_triggers(weights: dict, triggers: list) -> dict: def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions): - if option_key in game_weights: - try: + try: + if option_key in game_weights: if not option.supports_weighting: player_option = option.from_any(game_weights[option_key]) else: player_option = option.from_any(get_choice(option_key, game_weights)) - setattr(ret, option_key, player_option) - except Exception as e: - raise Exception(f"Error generating option {option_key} in {ret.game}") from e else: - player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options) + player_option = option.from_any(option.default) # call the from_any here to support default "random" + setattr(ret, option_key, player_option) + except Exception as e: + raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e else: - setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random" + player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options) def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses): diff --git a/MultiServer.py b/MultiServer.py index a92edf7660a9..9ee6a8032c1f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -803,14 +803,25 @@ async def on_client_disconnected(ctx: Context, client: Client): await on_client_left(ctx, client) +_non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"} +""" { tag: ui_message } """ + + async def on_client_joined(ctx: Context, client: Client): if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN: update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) version_str = '.'.join(str(x) for x in client.version) - verb = "tracking" if "Tracker" in client.tags else "playing" + + for tag, verb in _non_game_messages.items(): + if tag in client.tags: + final_verb = verb + break + else: + final_verb = "playing" + ctx.broadcast_text_all( f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) " - f"{verb} {ctx.games[client.slot]} has joined. " + f"{final_verb} {ctx.games[client.slot]} has joined. " f"Client({version_str}), {client.tags}.", {"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags}) ctx.notify_client(client, "Now that you are connected, " @@ -825,8 +836,19 @@ async def on_client_left(ctx: Context, client: Client): if len(ctx.clients[client.team][client.slot]) < 1: update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) + + version_str = '.'.join(str(x) for x in client.version) + + for tag, verb in _non_game_messages.items(): + if tag in client.tags: + final_verb = f"stopped {verb}" + break + else: + final_verb = "left" + ctx.broadcast_text_all( - "%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1), + f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. " + f"Client({version_str}), {client.tags}.", {"type": "Part", "team": client.team, "slot": client.slot}) @@ -1631,7 +1653,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): else: team, slot = ctx.connect_names[args['name']] game = ctx.games[slot] - ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game") + + ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"]) + if not ignore_game and args['game'] != game: errors.add('InvalidGame') minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot] diff --git a/Options.py b/Options.py index fc6335899d02..1eb0afeeeecb 100644 --- a/Options.py +++ b/Options.py @@ -21,6 +21,10 @@ import pathlib +class OptionError(ValueError): + pass + + class Visibility(enum.IntFlag): none = 0b0000 template = 0b0001 diff --git a/setup.py b/setup.py index bfc16337e18b..3e128eec7e55 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # 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>=6.15.16,<7' + requirement = 'cx-Freeze>=7.0.0' import pkg_resources try: pkg_resources.require(requirement) @@ -228,8 +228,8 @@ def finalize_options(self): # Override cx_Freeze's build_exe command for pre and post build steps -class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): - user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ +class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): + user_options = cx_Freeze.command.build_exe.build_exe.user_options + [ ('yes', 'y', 'Answer "yes" to all questions.'), ('extra-data=', None, 'Additional files to add.'), ] diff --git a/test/general/__init__.py b/test/general/__init__.py index fe890e0b340b..1d4fc80c3e55 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,7 +1,7 @@ from argparse import Namespace from typing import List, Optional, Tuple, Type, Union -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region from worlds.AutoWorld import World, call_all gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") @@ -17,19 +17,21 @@ def setup_solo_multiworld( :param steps: The gen steps that should be called on the generated multiworld before returning. Default calls steps through pre_fill :param seed: The seed to be used when creating this multiworld + :return: The generated multiworld """ return setup_multiworld(world_type, steps, seed) def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps, - seed: Optional[int] = None) -> MultiWorld: + seed: Optional[int] = None) -> MultiWorld: """ Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and calling the provided gen steps. - :param worlds: type/s of worlds to generate a multiworld for - :param steps: gen steps that should be called before returning. Default calls through pre_fill + :param worlds: Type/s of worlds to generate a multiworld for + :param steps: Gen steps that should be called before returning. Default calls through pre_fill :param seed: The seed to be used when creating this multiworld + :return: The generated multiworld """ if not isinstance(worlds, list): worlds = [worlds] @@ -49,3 +51,59 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple for step in steps: call_all(multiworld, step) return multiworld + + +class TestWorld(World): + game = f"Test Game" + item_name_to_id = {} + location_name_to_id = {} + hidden = True + + +def generate_test_multiworld(players: int = 1) -> MultiWorld: + """ + Generates a multiworld using a special Test Case World class, and seed of 0. + + :param players: Number of players to generate the multiworld for + :return: The generated test multiworld + """ + multiworld = setup_multiworld([TestWorld] * players, seed=0) + multiworld.regions += [Region("Menu", player_id + 1, multiworld) for player_id in range(players)] + + return multiworld + + +def generate_locations(count: int, player_id: int, region: Region, address: Optional[int] = None, + tag: str = "") -> List[Location]: + """ + Generates the specified amount of locations for the player and adds them to the specified region. + + :param count: Number of locations to create + :param player_id: ID of the player to create the locations for + :param address: Address for the specified locations. They will all share the same address if multiple are created + :param region: Parent region to add these locations to + :param tag: Tag to add to the name of the generated locations + :return: List containing the created locations + """ + prefix = f"player{player_id}{tag}_location" + + locations = [Location(player_id, f"{prefix}{i}", address, region) for i in range(count)] + region.locations += locations + return locations + + +def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]: + """ + Generates the specified amount of items for the target player. + + :param count: The amount of items to create + :param player_id: ID of the player to create the items for + :param advancement: Whether the created items should be advancement + :param code: The code the items should be created with + :return: List containing the created items + """ + item_type = "prog" if advancement else "" + classification = ItemClassification.progression if advancement else ItemClassification.filler + + items = [Item(f"player{player_id}_{item_type}item{i}", classification, code, player_id) for i in range(count)] + return items diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 7b004db61fee..485007ff0d56 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -1,41 +1,15 @@ from typing import List, Iterable import unittest -import Options from Options import Accessibility -from worlds.AutoWorld import World +from test.general import generate_items, generate_locations, generate_test_multiworld from Fill import FillError, balance_multiworld_progression, fill_restrictive, \ distribute_early_items, distribute_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \ - ItemClassification, CollectionState + ItemClassification from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule -def generate_multiworld(players: int = 1) -> MultiWorld: - multiworld = MultiWorld(players) - multiworld.set_seed(0) - multiworld.player_name = {} - multiworld.state = CollectionState(multiworld) - for i in range(players): - player_id = i+1 - world = World(multiworld, player_id) - multiworld.game[player_id] = f"Game {player_id}" - multiworld.worlds[player_id] = world - multiworld.player_name[player_id] = "Test Player " + str(player_id) - region = Region("Menu", player_id, multiworld, "Menu Region Hint") - multiworld.regions.append(region) - for option_key, option in Options.PerGameCommonOptions.type_hints.items(): - if hasattr(multiworld, option_key): - getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default"))) - else: - setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))}) - # TODO - remove this loop once all worlds use options dataclasses - world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id] - for option_key in world.options_dataclass.type_hints}) - - return multiworld - - class PlayerDefinition(object): multiworld: MultiWorld id: int @@ -55,12 +29,12 @@ def __init__(self, multiworld: MultiWorld, id: int, menu: Region, locations: Lis self.regions = [menu] def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region: - region_tag = "_region" + str(len(self.regions)) - region_name = "player" + str(self.id) + region_tag - region = Region("player" + str(self.id) + region_tag, self.id, self.multiworld) - self.locations += generate_locations(size, self.id, None, region, region_tag) + region_tag = f"_region{len(self.regions)}" + region_name = f"player{self.id}{region_tag}" + region = Region(f"player{self.id}{region_tag}", self.id, self.multiworld) + self.locations += generate_locations(size, self.id, region, None, region_tag) - entrance = Entrance(self.id, region_name + "_entrance", parent) + entrance = Entrance(self.id, f"{region_name}_entrance", parent) parent.exits.append(entrance) entrance.connect(region) entrance.access_rule = access_rule @@ -94,7 +68,7 @@ def region_contains(region: Region, item: Item) -> bool: def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition: menu = multiworld.get_region("Menu", player_id) - locations = generate_locations(location_count, player_id, None, menu) + locations = generate_locations(location_count, player_id, menu, None) prog_items = generate_items(prog_item_count, player_id, True) multiworld.itempool += prog_items basic_items = generate_items(basic_item_count, player_id, False) @@ -103,28 +77,6 @@ def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items) -def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]: - locations = [] - prefix = "player" + str(player_id) + tag + "_location" - for i in range(count): - name = prefix + str(i) - location = Location(player_id, name, address, region) - locations.append(location) - region.locations.append(location) - return locations - - -def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]: - items = [] - item_type = "prog" if advancement else "" - for i in range(count): - name = "player" + str(player_id) + "_" + item_type + "item" + str(i) - items.append(Item(name, - ItemClassification.progression if advancement else ItemClassification.filler, - code, player_id)) - return items - - def names(objs: list) -> Iterable[str]: return map(lambda o: o.name, objs) @@ -132,7 +84,7 @@ def names(objs: list) -> Iterable[str]: class TestFillRestrictive(unittest.TestCase): def test_basic_fill(self): """Tests `fill_restrictive` fills and removes the locations and items from their respective lists""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] @@ -150,7 +102,7 @@ def test_basic_fill(self): def test_ordered_fill(self): """Tests `fill_restrictive` fulfills set rules""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items locations = player1.locations @@ -167,7 +119,7 @@ def test_ordered_fill(self): def test_partial_fill(self): """Tests that `fill_restrictive` returns unfilled locations""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 3, 2) item0 = player1.prog_items[0] @@ -193,7 +145,7 @@ def test_partial_fill(self): def test_minimal_fill(self): """Test that fill for minimal player can have unreachable items""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items @@ -218,7 +170,7 @@ def test_minimal_mixed_fill(self): the non-minimal player get all items. """ - multiworld = generate_multiworld(2) + multiworld = generate_test_multiworld(2) player1 = generate_player_data(multiworld, 1, 3, 3) player2 = generate_player_data(multiworld, 2, 3, 3) @@ -245,11 +197,11 @@ def test_minimal_mixed_fill(self): # all of player2's locations and items should be accessible (not all of player1's) for item in player2.prog_items: self.assertTrue(multiworld.state.has(item.name, player2.id), - f'{item} is unreachable in {item.location}') + f"{item} is unreachable in {item.location}") def test_reversed_fill(self): """Test a different set of rules can be satisfied""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] @@ -268,7 +220,7 @@ def test_reversed_fill(self): def test_multi_step_fill(self): """Test that fill is able to satisfy multiple spheres""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 4, 4) items = player1.prog_items @@ -293,7 +245,7 @@ def test_multi_step_fill(self): def test_impossible_fill(self): """Test that fill raises an error when it can't place any items""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items locations = player1.locations @@ -310,7 +262,7 @@ def test_impossible_fill(self): def test_circular_fill(self): """Test that fill raises an error when it can't place all items""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 3, 3) item0 = player1.prog_items[0] @@ -331,7 +283,7 @@ def test_circular_fill(self): def test_competing_fill(self): """Test that fill raises an error when it can't place items in a way to satisfy the conditions""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] @@ -348,7 +300,7 @@ def test_competing_fill(self): def test_multiplayer_fill(self): """Test that items can be placed across worlds""" - multiworld = generate_multiworld(2) + multiworld = generate_test_multiworld(2) player1 = generate_player_data(multiworld, 1, 2, 2) player2 = generate_player_data(multiworld, 2, 2, 2) @@ -369,7 +321,7 @@ def test_multiplayer_fill(self): def test_multiplayer_rules_fill(self): """Test that fill across worlds satisfies the rules""" - multiworld = generate_multiworld(2) + multiworld = generate_test_multiworld(2) player1 = generate_player_data(multiworld, 1, 2, 2) player2 = generate_player_data(multiworld, 2, 2, 2) @@ -393,7 +345,7 @@ def test_multiplayer_rules_fill(self): def test_restrictive_progress(self): """Test that various spheres with different requirements can be filled""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, prog_item_count=25) items = player1.prog_items.copy() multiworld.completion_condition[player1.id] = lambda state: state.has_all( @@ -417,7 +369,7 @@ def test_restrictive_progress(self): def test_swap_to_earlier_location_with_item_rule(self): """Test that item swap happens and works as intended""" # test for PR#1109 - multiworld = generate_multiworld(1) + multiworld = generate_test_multiworld(1) player1 = generate_player_data(multiworld, 1, 4, 4) locations = player1.locations[:] # copy required items = player1.prog_items[:] # copy required @@ -442,7 +394,7 @@ def test_swap_to_earlier_location_with_item_rule(self): def test_swap_to_earlier_location_with_item_rule2(self): """Test that swap works before all items are placed""" - multiworld = generate_multiworld(1) + multiworld = generate_test_multiworld(1) player1 = generate_player_data(multiworld, 1, 5, 5) locations = player1.locations[:] # copy required items = player1.prog_items[:] # copy required @@ -484,7 +436,7 @@ def test_swap_to_earlier_location_with_item_rule2(self): def test_double_sweep(self): """Test that sweep doesn't duplicate Event items when sweeping""" # test for PR1114 - multiworld = generate_multiworld(1) + multiworld = generate_test_multiworld(1) player1 = generate_player_data(multiworld, 1, 1, 1) location = player1.locations[0] location.address = None @@ -498,7 +450,7 @@ def test_double_sweep(self): def test_correct_item_instance_removed_from_pool(self): """Test that a placed item gets removed from the submitted pool""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data(multiworld, 1, 2, 2) player1.prog_items[0].name = "Different_item_instance_but_same_item_name" @@ -515,7 +467,7 @@ def test_correct_item_instance_removed_from_pool(self): class TestDistributeItemsRestrictive(unittest.TestCase): def test_basic_distribute(self): """Test that distribute_items_restrictive is deterministic""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -535,7 +487,7 @@ def test_basic_distribute(self): def test_excluded_distribute(self): """Test that distribute_items_restrictive doesn't put advancement items on excluded locations""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -550,7 +502,7 @@ def test_excluded_distribute(self): def test_non_excluded_item_distribute(self): """Test that useful items aren't placed on excluded locations""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -565,7 +517,7 @@ def test_non_excluded_item_distribute(self): def test_too_many_excluded_distribute(self): """Test that fill fails if it can't place all progression items due to too many excluded locations""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -578,7 +530,7 @@ def test_too_many_excluded_distribute(self): def test_non_excluded_item_must_distribute(self): """Test that fill fails if it can't place useful items due to too many excluded locations""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -593,7 +545,7 @@ def test_non_excluded_item_must_distribute(self): def test_priority_distribute(self): """Test that priority locations receive advancement items""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -608,7 +560,7 @@ def test_priority_distribute(self): def test_excess_priority_distribute(self): """Test that if there's more priority locations than advancement items, they can still fill""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations @@ -623,7 +575,7 @@ def test_excess_priority_distribute(self): def test_multiple_world_priority_distribute(self): """Test that priority fill can be satisfied for multiple worlds""" - multiworld = generate_multiworld(3) + multiworld = generate_test_multiworld(3) player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) player2 = generate_player_data( @@ -653,7 +605,7 @@ def test_multiple_world_priority_distribute(self): def test_can_remove_locations_in_fill_hook(self): """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, 4, prog_item_count=2, basic_item_count=2) @@ -673,12 +625,12 @@ def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations): def test_seed_robust_to_item_order(self): """Test deterministic fill""" - mw1 = generate_multiworld() + mw1 = generate_test_multiworld() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) distribute_items_restrictive(mw1) - mw2 = generate_multiworld() + mw2 = generate_test_multiworld() gen2 = generate_player_data( mw2, 1, 4, prog_item_count=2, basic_item_count=2) mw2.itempool.append(mw2.itempool.pop(0)) @@ -691,12 +643,12 @@ def test_seed_robust_to_item_order(self): def test_seed_robust_to_location_order(self): """Test deterministic fill even if locations in a region are reordered""" - mw1 = generate_multiworld() + mw1 = generate_test_multiworld() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) distribute_items_restrictive(mw1) - mw2 = generate_multiworld() + mw2 = generate_test_multiworld() gen2 = generate_player_data( mw2, 1, 4, prog_item_count=2, basic_item_count=2) reg = mw2.get_region("Menu", gen2.id) @@ -710,7 +662,7 @@ def test_seed_robust_to_location_order(self): def test_can_reserve_advancement_items_for_general_fill(self): """Test that priority locations fill still satisfies item rules""" - multiworld = generate_multiworld() + multiworld = generate_test_multiworld() player1 = generate_player_data( multiworld, 1, location_count=5, prog_item_count=5) items = player1.prog_items @@ -727,7 +679,7 @@ def test_can_reserve_advancement_items_for_general_fill(self): def test_non_excluded_local_items(self): """Test that local items get placed locally in a multiworld""" - multiworld = generate_multiworld(2) + multiworld = generate_test_multiworld(2) player1 = generate_player_data( multiworld, 1, location_count=5, basic_item_count=5) player2 = generate_player_data( @@ -748,7 +700,7 @@ def test_non_excluded_local_items(self): def test_early_items(self) -> None: """Test that the early items API successfully places items early""" - mw = generate_multiworld(2) + mw = generate_test_multiworld(2) player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5) player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5) mw.early_items[1][player1.basic_items[0].name] = 1 @@ -803,11 +755,11 @@ def assertRegionContains(self, region: Region, item: Item) -> bool: if location.item and location.item == item: return True - self.fail("Expected " + region.name + " to contain " + item.name + - "\n Contains" + str(list(map(lambda location: location.item, region.locations)))) + self.fail(f"Expected {region.name} to contain {item.name}.\n" + f"Contains{list(map(lambda location: location.item, region.locations))}") def setUp(self) -> None: - multiworld = generate_multiworld(2) + multiworld = generate_test_multiworld(2) self.multiworld = multiworld player1 = generate_player_data( multiworld, 1, prog_item_count=2, basic_item_count=40) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 4057cded9a5b..1359bea5ce6d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -199,8 +199,14 @@ def create_regions(self): self.multiworld.regions.append(menu_region) # wp_exclusions = self.white_palace_exclusions() + # check for any goal that godhome events are relevant to + all_event_names = event_names.copy() + if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + from .GodhomeData import godhome_event_names + all_event_names.update(set(godhome_event_names)) + # Link regions - for event_name in event_names: + for event_name in all_event_names: #if event_name in wp_exclusions: # continue loc = HKLocation(self.player, event_name, None, menu_region) @@ -307,12 +313,6 @@ def _add(item_name: str, location_name: str, randomized: bool): randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) - # check for any goal that godhome events are relevant to - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: - from .GodhomeData import godhome_event_names - for item_name in godhome_event_names: - _add(item_name, item_name, False) - for shop, locations in self.created_multi_locations.items(): for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): loc = self.create_location(shop) diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index a6e53e761d49..5ffedee36799 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -10,6 +10,7 @@ class LocationClassification(Flag): normal = auto() reduced = auto() insanity = auto() + small_sphere_one = auto() class LocationData(NamedTuple): @@ -47,6 +48,9 @@ def load_location_data(): if not panel.exclude_reduce: classification |= LocationClassification.reduced + if room_name == "Starting Room": + classification |= LocationClassification.small_sphere_one + ALL_LOCATION_TABLE[location_name] = \ LocationData(get_panel_location_id(room_name, panel_name), room_name, [RoomAndPanel(None, panel_name)], classification) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 96e9869d3731..7019269193c0 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -236,9 +236,12 @@ def __init__(self, world: "LingoWorld"): elif location_checks == LocationChecks.option_insanity: location_classification = LocationClassification.insanity + if door_shuffle != ShuffleDoors.option_none and not early_color_hallways: + location_classification |= LocationClassification.small_sphere_one + for location_name, location_data in ALL_LOCATION_TABLE.items(): if location_name != self.victory_condition: - if location_classification not in location_data.classification: + if not (location_classification & location_data.classification): continue self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) @@ -275,7 +278,7 @@ def __init__(self, world: "LingoWorld"): "iterations. This is very unlikely to happen on its own, and probably indicates some " "kind of logic error.") - if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ + if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \ and not early_color_hallways and world.multiworld.players > 1: # Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is # only three checks. In a multiplayer situation, this can be frustrating for the player because they are diff --git a/worlds/noita/locations.py b/worlds/noita/locations.py index 926a502fbca4..5dd87b5b0387 100644 --- a/worlds/noita/locations.py +++ b/worlds/noita/locations.py @@ -12,7 +12,7 @@ class NoitaLocation(Location): class LocationData(NamedTuple): id: int flag: int = 0 - ltype: str = "shop" + ltype: str = "Shop" class LocationFlag(IntEnum): @@ -25,7 +25,7 @@ class LocationFlag(IntEnum): # Mapping of items in each region. # Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions. -# ltype key: "chest" = Hidden Chests, "pedestal" = Pedestals, "boss" = Boss, "orb" = Orb. +# ltype key: "Chest" = Hidden Chests, "Pedestal" = Pedestals, "Boss" = Boss, "Orb" = Orb. # 110000-110671 location_region_mapping: Dict[str, Dict[str, LocationData]] = { "Coal Pits Holy Mountain": { @@ -91,117 +91,118 @@ class LocationFlag(IntEnum): "Secret Shop Item 4": LocationData(110045), }, "The Sky": { - "Kivi": LocationData(110670, LocationFlag.main_world, "boss"), + "Kivi": LocationData(110670, LocationFlag.main_world, "Boss"), }, "Floating Island": { - "Floating Island Orb": LocationData(110658, LocationFlag.main_path, "orb"), + "Floating Island Orb": LocationData(110658, LocationFlag.main_path, "Orb"), }, "Pyramid": { - "Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "boss"), - "Pyramid Orb": LocationData(110659, LocationFlag.main_world, "orb"), - "Sandcave Orb": LocationData(110662, LocationFlag.main_world, "orb"), + "Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "Boss"), + "Pyramid Orb": LocationData(110659, LocationFlag.main_world, "Orb"), + "Sandcave Orb": LocationData(110662, LocationFlag.main_world, "Orb"), }, "Overgrown Cavern": { - "Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "chest"), - "Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "pedestal"), + "Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "Chest"), + "Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "Pedestal"), }, "Lake": { - "Syväolento": LocationData(110651, LocationFlag.main_world, "boss"), - "Tapion vasalli": LocationData(110669, LocationFlag.main_world, "boss"), + "Syväolento": LocationData(110651, LocationFlag.main_world, "Boss"), + "Tapion vasalli": LocationData(110669, LocationFlag.main_world, "Boss"), }, "Frozen Vault": { - "Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "orb"), - "Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "chest"), - "Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "pedestal"), + "Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "Orb"), + "Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "Chest"), + "Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "Pedestal"), }, "Mines": { - "Mines Chest": LocationData(110046, LocationFlag.main_path, "chest"), - "Mines Pedestal": LocationData(110066, LocationFlag.main_path, "pedestal"), + "Mines Chest": LocationData(110046, LocationFlag.main_path, "Chest"), + "Mines Pedestal": LocationData(110066, LocationFlag.main_path, "Pedestal"), }, # Collapsed Mines is a very small area, combining it with the Mines. Leaving this here as a reminder + "Ancient Laboratory": { - "Ylialkemisti": LocationData(110656, LocationFlag.side_path, "boss"), + "Ylialkemisti": LocationData(110656, LocationFlag.side_path, "Boss"), }, "Abyss Orb Room": { - "Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "boss"), - "Abyss Orb": LocationData(110665, LocationFlag.main_path, "orb"), + "Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "Boss"), + "Abyss Orb": LocationData(110665, LocationFlag.main_path, "Orb"), }, "Below Lava Lake": { - "Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "orb"), + "Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "Orb"), }, "Coal Pits": { - "Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "chest"), - "Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "pedestal"), + "Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "Chest"), + "Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "Pedestal"), }, "Fungal Caverns": { - "Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "chest"), - "Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "pedestal"), + "Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "Chest"), + "Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "Pedestal"), }, "Snowy Depths": { - "Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "chest"), - "Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "pedestal"), + "Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "Chest"), + "Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "Pedestal"), }, "Magical Temple": { - "Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "orb"), + "Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "Orb"), }, "Hiisi Base": { - "Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "chest"), - "Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "pedestal"), + "Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "Chest"), + "Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "Pedestal"), }, "Underground Jungle": { - "Suomuhauki": LocationData(110648, LocationFlag.main_path, "boss"), - "Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "chest"), - "Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "pedestal"), + "Suomuhauki": LocationData(110648, LocationFlag.main_path, "Boss"), + "Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "Chest"), + "Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "Pedestal"), }, "Lukki Lair": { - "Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "orb"), - "Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "chest"), - "Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "pedestal"), + "Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "Orb"), + "Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "Chest"), + "Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "Pedestal"), }, "The Vault": { - "The Vault Chest": LocationData(110366, LocationFlag.main_path, "chest"), - "The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "pedestal"), + "The Vault Chest": LocationData(110366, LocationFlag.main_path, "Chest"), + "The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "Pedestal"), }, "Temple of the Art": { - "Gate Guardian": LocationData(110652, LocationFlag.main_path, "boss"), - "Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "chest"), - "Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "pedestal"), + "Gate Guardian": LocationData(110652, LocationFlag.main_path, "Boss"), + "Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "Chest"), + "Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "Pedestal"), }, "The Tower": { - "The Tower Chest": LocationData(110606, LocationFlag.main_world, "chest"), - "The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "pedestal"), + "The Tower Chest": LocationData(110606, LocationFlag.main_world, "Chest"), + "The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "Pedestal"), }, "Wizards' Den": { - "Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "boss"), - "Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "orb"), - "Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "chest"), - "Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "pedestal"), + "Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "Boss"), + "Wizards' Den Orb": LocationData(110668, LocationFlag.main_world, "Orb"), + "Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "Chest"), + "Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "Pedestal"), }, "Powerplant": { - "Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "boss"), - "Power Plant Chest": LocationData(110486, LocationFlag.main_world, "chest"), - "Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "pedestal"), + "Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "Boss"), + "Power Plant Chest": LocationData(110486, LocationFlag.main_world, "Chest"), + "Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "Pedestal"), }, "Snow Chasm": { - "Unohdettu": LocationData(110653, LocationFlag.main_world, "boss"), - "Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "orb"), + "Unohdettu": LocationData(110653, LocationFlag.main_world, "Boss"), + "Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "Orb"), }, "Meat Realm": { - "Meat Realm Chest": LocationData(110086, LocationFlag.main_world, "chest"), - "Meat Realm Pedestal": LocationData(110106, LocationFlag.main_world, "pedestal"), - "Limatoukka": LocationData(110647, LocationFlag.main_world, "boss"), + "Meat Realm Chest": LocationData(110086, LocationFlag.main_world, "Chest"), + "Meat Realm Pedestal": LocationData(110106, LocationFlag.main_world, "Pedestal"), + "Limatoukka": LocationData(110647, LocationFlag.main_world, "Boss"), }, "West Meat Realm": { - "Kolmisilmän sydän": LocationData(110671, LocationFlag.main_world, "boss"), + "Kolmisilmän sydän": LocationData(110671, LocationFlag.main_world, "Boss"), }, "The Laboratory": { - "Kolmisilmä": LocationData(110646, LocationFlag.main_path, "boss"), + "Kolmisilmä": LocationData(110646, LocationFlag.main_path, "Boss"), }, "Friend Cave": { - "Toveri": LocationData(110654, LocationFlag.main_world, "boss"), + "Toveri": LocationData(110654, LocationFlag.main_world, "Boss"), }, "The Work (Hell)": { - "The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "orb"), + "The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "Orb"), }, } @@ -212,18 +213,20 @@ def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, return {f"{location_name} {i+1}": base_id + i for i in range(amt)} -location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(), - "pedestal": set()} +location_name_groups: Dict[str, Set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(), + "Pedestal": set()} location_name_to_id: Dict[str, int] = {} -for location_group in location_region_mapping.values(): +for region_name, location_group in location_region_mapping.items(): + location_name_groups[region_name] = set() for locname, locinfo in location_group.items(): # Iterating the hidden chest and pedestal locations here to avoid clutter above - amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1 + amount = 20 if locinfo.ltype in ["Chest", "Pedestal"] else 1 entries = make_location_range(locname, locinfo.id, amount) location_name_to_id.update(entries) location_name_groups[locinfo.ltype].update(entries.keys()) + location_name_groups[region_name].update(entries.keys()) shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name} diff --git a/worlds/noita/regions.py b/worlds/noita/regions.py index a556b102cc04..184cd96018cf 100644 --- a/worlds/noita/regions.py +++ b/worlds/noita/regions.py @@ -15,14 +15,14 @@ def create_locations(world: "NoitaWorld", region: Region) -> None: location_type = location_data.ltype flag = location_data.flag - is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks - is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks + is_orb_allowed = location_type == "Orb" and flag <= world.options.orbs_as_checks + is_boss_allowed = location_type == "Boss" and flag <= world.options.bosses_as_checks amount = 0 if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: amount = 1 - elif location_type == "chest" and flag <= world.options.path_option: + elif location_type == "Chest" and flag <= world.options.path_option: amount = world.options.hidden_chests.value - elif location_type == "pedestal" and flag <= world.options.path_option: + elif location_type == "Pedestal" and flag <= world.options.path_option: amount = world.options.pedestal_checks.value region.add_locations(locations.make_location_range(location_name, location_data.id, amount), diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index e3f6d3c3013b..2ae54d5e0c14 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -21,7 +21,7 @@ clear it. ## Optional Software -- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), for use with +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases) ## Generating and Patching a Game @@ -64,7 +64,7 @@ perfectly safe to make progress offline; everything will re-sync when you reconn Pokémon Emerald has a fully functional map tracker that supports auto-tracking. -1. Download [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) and +1. Download [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) and [PopTracker](https://github.com/black-sliver/PopTracker/releases). 2. Put the tracker pack into packs/ in your PopTracker install. 3. Open PopTracker, and load the Pokémon Emerald pack. diff --git a/worlds/pokemon_emerald/docs/setup_es.md b/worlds/pokemon_emerald/docs/setup_es.md index 65a74a9ddc70..28c3a4a01a65 100644 --- a/worlds/pokemon_emerald/docs/setup_es.md +++ b/worlds/pokemon_emerald/docs/setup_es.md @@ -21,7 +21,7 @@ limpiarlas, selecciona el atajo y presiona la tecla Esc. ## Software Opcional -- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), para usar con +- [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 @@ -65,7 +65,7 @@ jugar de manera offline; se sincronizará todo cuando te vuelvas a conectar. Pokémon Emerald tiene un Map Tracker completamente funcional que soporta auto-tracking. -1. Descarga [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) y +1. Descarga [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) y [PopTracker](https://github.com/black-sliver/PopTracker/releases). 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. diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py index 99b6448aff35..4dece46411bf 100644 --- a/worlds/sc2/MissionTables.py +++ b/worlds/sc2/MissionTables.py @@ -650,7 +650,7 @@ class SC2CampaignGoal(NamedTuple): SC2Campaign.PROLOGUE: SC2CampaignGoal(SC2Mission.EVIL_AWOKEN, "Evil Awoken: Victory"), SC2Campaign.LOTV: SC2CampaignGoal(SC2Mission.SALVATION, "Salvation: Victory"), SC2Campaign.EPILOGUE: None, - SC2Campaign.NCO: None, + SC2Campaign.NCO: SC2CampaignGoal(SC2Mission.END_GAME, "End Game: Victory"), } campaign_alt_final_mission_locations: Dict[SC2Campaign, Dict[SC2Mission, str]] = { @@ -683,7 +683,6 @@ class SC2CampaignGoal(NamedTuple): SC2Mission.THE_ESSENCE_OF_ETERNITY: "The Essence of Eternity: Victory", }, SC2Campaign.NCO: { - SC2Mission.END_GAME: "End Game: Victory", SC2Mission.FLASHPOINT: "Flashpoint: Victory", SC2Mission.DARK_SKIES: "Dark Skies: Victory", SC2Mission.NIGHT_TERRORS: "Night Terrors: Victory", @@ -709,10 +708,10 @@ def get_goal_location(mission: SC2Mission) -> Union[str, None]: return primary_campaign_goal.location campaign_alt_goals = campaign_alt_final_mission_locations[campaign] - if campaign_alt_goals is not None: + if campaign_alt_goals is not None and mission in campaign_alt_goals: return campaign_alt_goals.get(mission) - return None + return mission.mission_name + ": Victory" def get_campaign_potential_goal_missions(campaign: SC2Campaign) -> List[SC2Mission]: diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index e94dc4e214c8..f5f6faa96d62 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Set, Union, Tuple +from typing import Callable, Dict, List, Set, Union, Tuple, Optional from BaseClasses import Item, Location from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, progressive_if_nco, \ progressive_if_ext, spear_of_adun_calldowns, spear_of_adun_castable_passives, nova_equipment @@ -69,21 +69,39 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]: return mission_pools # Finding the goal map - goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns} - goal_level = max(goal_priorities.values()) - candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] - candidate_campaigns.sort(key=lambda it: it.id) - goal_campaign = world.random.choice(candidate_campaigns) - primary_goal = campaign_final_mission_locations[goal_campaign] - if primary_goal is None or primary_goal.mission in excluded_missions: - # No primary goal or its mission is excluded - candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys()) - candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions] - if len(candidate_missions) == 0: - raise Exception("There are no valid goal missions. Please exclude fewer missions.") - goal_mission = world.random.choice(candidate_missions) + goal_mission: Optional[SC2Mission] = None + if mission_order_type in campaign_depending_orders: + # Prefer long campaigns over shorter ones and harder missions over easier ones + goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns} + goal_level = max(goal_priorities.values()) + candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level] + candidate_campaigns.sort(key=lambda it: it.id) + + goal_campaign = world.random.choice(candidate_campaigns) + primary_goal = campaign_final_mission_locations[goal_campaign] + if primary_goal is None or primary_goal.mission in excluded_missions: + # No primary goal or its mission is excluded + candidate_missions = list(campaign_alt_final_mission_locations[goal_campaign].keys()) + candidate_missions = [mission for mission in candidate_missions if mission not in excluded_missions] + if len(candidate_missions) == 0: + raise Exception("There are no valid goal missions. Please exclude fewer missions.") + goal_mission = world.random.choice(candidate_missions) + else: + goal_mission = primary_goal.mission else: - goal_mission = primary_goal.mission + # Find one of the missions with the hardest difficulty + available_missions: List[SC2Mission] = \ + [mission for mission in SC2Mission + if (mission not in excluded_missions and mission.campaign in enabled_campaigns)] + available_missions.sort(key=lambda it: it.id) + # Loop over pools, from hardest to easiest + for mission_pool in range(MissionPools.VERY_HARD, MissionPools.STARTER - 1, -1): + pool_missions: List[SC2Mission] = [mission for mission in available_missions if mission.pool == mission_pool] + if pool_missions: + goal_mission = world.random.choice(pool_missions) + break + if goal_mission is None: + raise Exception("There are no valid goal missions. Please exclude fewer missions.") # Excluding missions for difficulty, mission_pool in mission_pools.items(): diff --git a/worlds/tloz/Items.py b/worlds/tloz/Items.py index d896d11d770b..b421b740012c 100644 --- a/worlds/tloz/Items.py +++ b/worlds/tloz/Items.py @@ -24,7 +24,7 @@ class ItemData(typing.NamedTuple): "Red Candle": ItemData(107, progression), "Book of Magic": ItemData(108, progression), "Magical Key": ItemData(109, useful), - "Red Ring": ItemData(110, useful), + "Red Ring": ItemData(110, progression), "Silver Arrow": ItemData(111, progression), "Sword": ItemData(112, progression), "White Sword": ItemData(113, progression), @@ -37,7 +37,7 @@ class ItemData(typing.NamedTuple): "Food": ItemData(120, progression), "Water of Life (Blue)": ItemData(121, useful), "Water of Life (Red)": ItemData(122, useful), - "Blue Ring": ItemData(123, useful), + "Blue Ring": ItemData(123, progression), "Triforce Fragment": ItemData(124, progression), "Power Bracelet": ItemData(125, useful), "Small Key": ItemData(126, filler), diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index ceb1041ba576..39c3b954f0d4 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -28,6 +28,7 @@ def set_rules(tloz_world: "TLoZWorld"): or location.name not in dangerous_weapon_locations: add_rule(world.get_location(location.name, player), lambda state: state.has_group("weapons", player)) + # This part of the loop sets up an expected amount of defense needed for each dungeon if i > 0: # Don't need an extra heart for Level 1 add_rule(world.get_location(location.name, player), lambda state, hearts=i: state.has("Heart Container", player, hearts) or diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 77324b2047b4..356af56ebd3e 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -140,7 +140,7 @@ def remove_filler(amount: int) -> None: if self.options.shuffle_ladders: ladder_count = 0 for item_name, item_data in item_table.items(): - if item_data.item_group == "ladders": + if item_data.item_group == "Ladders": items_to_create[item_name] = 1 ladder_count += 1 remove_filler(ladder_count) @@ -259,7 +259,7 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: name, connection = connection # for LS entrances, we just want to give the portal name if "(LS)" in name: - name, _ = name.split(" (LS) ") + name = name.split(" (LS) ", 1)[0] # was getting some cases like Library Grave -> Library Grave -> other place if name in portal_names and name != previous_name: previous_name = name diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 57a9167d1906..29a7255ea771 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -64,11 +64,8 @@ For the Entrance Randomizer: - The portal in the trophy room of the Old House is active from the start. - The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing. -## What item groups are there? -Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, icebolt, and progressive sword. - -## What location groups are there? -Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), shop, bosses (for the bosses with checks associated with them), hero relic (for the 6 hero grave checks), and ladders (for the ladder items when you have shuffle ladders enabled). +## Does this game have item and location groups? +Yes! To find what they are, open up the Archipelago Text Client while connected to a TUNIC session and type in `/item_groups` or `/location_groups`. ## Is Connection Plando supported? Yes. The host needs to enable it in their `host.yaml`, and the player's yaml needs to contain a plando_connections block. diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index dde142c88abc..6352d96bf407 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1452,7 +1452,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) # Beneath the Vault set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), - lambda state: state.has_group("melee weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), lambda state: has_lantern(state, player, options)) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 7483d55bf1cc..6efdeaa3eabb 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -13,158 +13,158 @@ class TunicItemData(NamedTuple): item_base_id = 509342400 item_table: Dict[str, TunicItemData] = { - "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "bombs"), - "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "bombs"), - "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "bombs"), - "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "bombs"), - "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "bombs"), - "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "bombs"), - "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "bombs"), - "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "bombs"), - "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "bombs"), - "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "bombs"), - "Lure": TunicItemData(ItemClassification.filler, 4, 10, "consumables"), - "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "consumables"), - "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "consumables"), - "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "consumables"), - "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "money"), - "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "consumables"), - "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "consumables"), - "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "consumables"), - "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "consumables"), - "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "consumables"), - "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "consumables"), + "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "Bombs"), + "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "Bombs"), + "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "Bombs"), + "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "Bombs"), + "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "Bombs"), + "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "Bombs"), + "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "Bombs"), + "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "Bombs"), + "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "Bombs"), + "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "Bombs"), + "Lure": TunicItemData(ItemClassification.filler, 4, 10, "Consumables"), + "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "Consumables"), + "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "Consumables"), + "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "Consumables"), + "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "Money"), + "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "Consumables"), + "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "Consumables"), + "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "Consumables"), + "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "Consumables"), + "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "Consumables"), + "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "Consumables"), "Fairy": TunicItemData(ItemClassification.progression, 20, 21), - "Stick": TunicItemData(ItemClassification.progression, 1, 22, "weapons"), - "Sword": TunicItemData(ItemClassification.progression, 3, 23, "weapons"), - "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "weapons"), - "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "weapons"), + "Stick": TunicItemData(ItemClassification.progression, 1, 22, "Weapons"), + "Sword": TunicItemData(ItemClassification.progression, 3, 23, "Weapons"), + "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "Weapons"), + "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "Weapons"), "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26), "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27), "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28), "Lantern": TunicItemData(ItemClassification.progression, 1, 29), - "Gun": TunicItemData(ItemClassification.useful, 1, 30, "weapons"), + "Gun": TunicItemData(ItemClassification.useful, 1, 30, "Weapons"), "Shield": TunicItemData(ItemClassification.useful, 1, 31), "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32), "Hourglass": TunicItemData(ItemClassification.useful, 1, 33), - "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "keys"), - "Key": TunicItemData(ItemClassification.progression, 2, 35, "keys"), - "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "keys"), - "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37, "potions"), - "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"), + "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "Keys"), + "Key": TunicItemData(ItemClassification.progression, 2, 35, "Keys"), + "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "Keys"), + "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37), + "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "Flask"), "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39), "Card Slot": TunicItemData(ItemClassification.useful, 4, 40), - "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "hexagons"), - "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "hexagons"), - "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "hexagons"), - "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "hexagons"), - "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "offerings"), - "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "offerings"), - "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "offerings"), - "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "offerings"), - "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "offerings"), - "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "offerings"), - "Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "hero relics"), - "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"), - "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"), - "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"), - "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"), - "Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "hero relics"), - "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "cards"), - "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "cards"), - "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "cards"), - "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "cards"), - "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "cards"), - "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "cards"), - "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "cards"), - "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "cards"), - "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "cards"), - "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "cards"), - "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "cards"), - "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "cards"), - "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "cards"), - "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "cards"), - "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "cards"), - "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "cards"), - "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "golden treasures"), - "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "golden treasures"), - "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "golden treasures"), - "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "golden treasures"), - "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "golden treasures"), - "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "golden treasures"), - "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "golden treasures"), - "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "golden treasures"), - "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "golden treasures"), - "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "golden treasures"), - "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "golden treasures"), - "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "golden treasures"), - "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85, "fool"), - "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "money"), - "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "money"), - "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "money"), - "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "money"), - "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "money"), - "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "money"), - "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "money"), - "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "money"), - "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "money"), - "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "money"), - "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "money"), - "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "money"), - "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "money"), - "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "money"), - "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "money"), - "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "money"), - "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "pages"), - "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "pages"), - "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "pages"), - "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "pages"), - "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "pages"), - "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "pages"), - "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "pages"), - "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "pages"), - "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "pages"), - "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "pages"), - "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "pages"), - "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "pages"), - "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "pages"), - "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "pages"), - "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "pages"), - "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "pages"), - "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "pages"), - "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "pages"), - "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "pages"), - "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "pages"), - "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "pages"), - "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "pages"), - "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "pages"), - "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "pages"), - "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "pages"), - "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"), - "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "pages"), - "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"), + "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "Hexagons"), + "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "Hexagons"), + "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "Hexagons"), + "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "Hexagons"), + "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "Offerings"), + "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "Offerings"), + "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "Offerings"), + "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"), + "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"), + "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"), + "Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "Hero Relics"), + "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "Hero Relics"), + "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "Hero Relics"), + "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "Hero Relics"), + "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "Hero Relics"), + "Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "Hero Relics"), + "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"), + "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"), + "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"), + "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "Cards"), + "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "Cards"), + "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "Cards"), + "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "Cards"), + "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "Cards"), + "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "Cards"), + "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "Cards"), + "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "Cards"), + "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "Cards"), + "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "Cards"), + "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "Cards"), + "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "Cards"), + "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "Cards"), + "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "Golden Treasures"), + "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "Golden Treasures"), + "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "Golden Treasures"), + "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "Golden Treasures"), + "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "Golden Treasures"), + "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "Golden Treasures"), + "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "Golden Treasures"), + "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "Golden Treasures"), + "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "Golden Treasures"), + "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "Golden Treasures"), + "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "Golden Treasures"), + "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "Golden Treasures"), + "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85), + "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "Money"), + "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "Money"), + "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "Money"), + "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "Money"), + "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "Money"), + "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "Money"), + "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "Money"), + "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "Money"), + "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "Money"), + "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "Money"), + "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "Money"), + "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "Money"), + "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "Money"), + "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "Money"), + "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "Money"), + "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "Money"), + "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "Pages"), + "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "Pages"), + "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "Pages"), + "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "Pages"), + "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "Pages"), + "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "Pages"), + "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "Pages"), + "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "Pages"), + "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "Pages"), + "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "Pages"), + "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "Pages"), + "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "Pages"), + "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "Pages"), + "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "Pages"), + "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "Pages"), + "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "Pages"), + "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "Pages"), + "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "Pages"), + "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "Pages"), + "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "Pages"), + "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "Pages"), + "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "Pages"), + "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "Pages"), + "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "Pages"), + "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "Pages"), + "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"), + "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"), + "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"), - "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "ladders"), - "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "ladders"), - "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "ladders"), - "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "ladders"), - "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "ladders"), - "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "ladders"), - "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "ladders"), - "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "ladders"), - "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "ladders"), - "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "ladders"), - "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "ladders"), - "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "ladders"), - "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "ladders"), - "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "ladders"), - "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "ladders"), - "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "ladders"), - "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "ladders"), - "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "ladders"), - "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "ladders"), - "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "ladders"), - "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "ladders"), + "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"), + "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"), + "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"), + "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "Ladders"), + "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "Ladders"), + "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "Ladders"), + "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "Ladders"), + "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "Ladders"), + "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "Ladders"), + "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "Ladders"), + "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "Ladders"), + "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "Ladders"), + "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "Ladders"), + "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "Ladders"), + "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "Ladders"), + "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "Ladders"), + "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "Ladders"), + "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "Ladders"), + "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "Ladders"), + "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "Ladders"), + "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), } fool_tiers: List[List[str]] = [ @@ -220,20 +220,23 @@ def get_item_group(item_name: str) -> str: # extra groups for the purpose of aliasing items extra_groups: Dict[str, Set[str]] = { - "laurels": {"Hero's Laurels"}, - "orb": {"Magic Orb"}, - "dagger": {"Magic Dagger"}, - "magic rod": {"Magic Wand"}, - "holy cross": {"Pages 42-43 (Holy Cross)"}, - "prayer": {"Pages 24-25 (Prayer)"}, - "icebolt": {"Pages 52-53 (Icebolt)"}, - "ice rod": {"Pages 52-53 (Icebolt)"}, - "melee weapons": {"Stick", "Sword", "Sword Upgrade"}, - "progressive sword": {"Sword Upgrade"}, - "abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"}, - "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, - "ladder to atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't - "ladders to bell": {"Ladders to West Bell"}, + "Laurels": {"Hero's Laurels"}, + "Orb": {"Magic Orb"}, + "Dagger": {"Magic Dagger"}, + "Wand": {"Magic Wand"}, + "Magic Rod": {"Magic Wand"}, + "Fire Rod": {"Magic Wand"}, + "Holy Cross": {"Pages 42-43 (Holy Cross)"}, + "Prayer": {"Pages 24-25 (Prayer)"}, + "Icebolt": {"Pages 52-53 (Icebolt)"}, + "Ice Rod": {"Pages 52-53 (Icebolt)"}, + "Melee Weapons": {"Stick", "Sword", "Sword Upgrade"}, + "Progressive Sword": {"Sword Upgrade"}, + "Abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"}, + "Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, + "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't + "Ladders to Bell": {"Ladders to West Bell"}, + "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell } item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 9974e60571c2..fdf662167953 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -1,11 +1,10 @@ -from typing import Dict, NamedTuple, Set, Optional, List +from typing import Dict, NamedTuple, Set, Optional class TunicLocationData(NamedTuple): region: str er_region: str # entrance rando region location_group: Optional[str] = None - location_groups: Optional[List[str]] = None location_base_id = 509342400 @@ -46,8 +45,8 @@ class TunicLocationData(NamedTuple): "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2 Lower"), "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"), "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), - "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="holy cross"), - "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="holy cross"), + "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"), + "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"), "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), @@ -65,18 +64,18 @@ class TunicLocationData(NamedTuple): "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"), "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"), "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"), - "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="holy cross"), + "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="Holy Cross"), "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"), "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"), "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"), "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"), - "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest", location_group="hero relic"), + "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"), "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"), "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"), "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), - "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="bosses"), + "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="Bosses"), "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"), - "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="holy cross"), + "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="Holy Cross"), "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), @@ -84,7 +83,7 @@ class TunicLocationData(NamedTuple): "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"), "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), - "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress", location_group="hero relic"), + "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"), @@ -101,8 +100,8 @@ class TunicLocationData(NamedTuple): "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"), "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"), "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="bosses"), - "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="holy cross"), + "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"), + "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"), "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"), "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"), @@ -110,7 +109,7 @@ class TunicLocationData(NamedTuple): "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"), "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"), "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"), - "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library", location_group="hero relic"), + "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"), "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"), "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"), "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), @@ -165,49 +164,49 @@ class TunicLocationData(NamedTuple): "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"), - "Shop - Potion 1": TunicLocationData("Overworld", "Shop", location_group="shop"), - "Shop - Potion 2": TunicLocationData("Overworld", "Shop", location_group="shop"), - "Shop - Coin 1": TunicLocationData("Overworld", "Shop", location_group="shop"), - "Shop - Coin 2": TunicLocationData("Overworld", "Shop", location_group="shop"), + "Shop - Potion 1": TunicLocationData("Overworld", "Shop"), + "Shop - Potion 2": TunicLocationData("Overworld", "Shop"), + "Shop - Coin 1": TunicLocationData("Overworld", "Shop"), + "Shop - Coin 2": TunicLocationData("Overworld", "Shop"), "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"), "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"), "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"), "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"), "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"), "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn Region"), - "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), - "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), - "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), - "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), - "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"), - "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"), - "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), - "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"), - "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), - "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"), - "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"), - "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), - "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"), - "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), - "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), - "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="holy cross"), - "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="holy cross"), - "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="holy cross"), - "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), - "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), - "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), - "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="holy cross"), - "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="holy cross"), - "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="holy cross"), - "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="holy cross"), - "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="holy cross"), - "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="holy cross"), - "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="holy cross"), - "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="holy cross"), - "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="holy cross"), - "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="holy cross"), + "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"), + "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"), + "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"), + "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="Well"), + "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"), + "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="Fairies"), + "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"), + "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"), + "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"), + "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"), + "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="Holy Cross"), + "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"), + "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="Holy Cross"), + "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"), + "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="Holy Cross"), + "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="Holy Cross"), + "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="Holy Cross"), + "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="Holy Cross"), + "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"), + "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"), + "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="Holy Cross"), + "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="Holy Cross"), + "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="Holy Cross"), + "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="Holy Cross"), + "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="Holy Cross"), + "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="Holy Cross"), + "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="Holy Cross"), + "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="Holy Cross"), + "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"), + "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"), + "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"), "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), - "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="holy cross"), + "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), @@ -225,7 +224,7 @@ class TunicLocationData(NamedTuple): "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry", location_group="hero relics"), + "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), @@ -246,7 +245,7 @@ class TunicLocationData(NamedTuple): "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="bosses"), + "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"), "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"), "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"), @@ -288,14 +287,14 @@ class TunicLocationData(NamedTuple): "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp Mid"), "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp Front"), "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp Mid"), - "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp", location_group="hero relic"), + "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"), "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), - "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), - "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), + "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"), - "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), + "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"), "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"), @@ -307,12 +306,12 @@ class TunicLocationData(NamedTuple): "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="bosses"), + "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"), "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"), "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), - "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden", location_group="hero relic"), + "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), } hexagon_locations: Dict[str, str] = { @@ -325,7 +324,7 @@ class TunicLocationData(NamedTuple): location_name_groups: Dict[str, Set[str]] = {} for loc_name, loc_data in location_table.items(): + loc_group_name = loc_name.split(" - ", 1)[0] + location_name_groups.setdefault(loc_group_name, set()).add(loc_name) if loc_data.location_group: - if loc_data.location_group not in location_name_groups.keys(): - location_name_groups[loc_data.location_group] = set() - location_name_groups[loc_data.location_group].add(loc_name) + location_name_groups.setdefault(loc_data.location_group, set()).add(loc_name)