diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 84f1338a..db0357da 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,25 +1,28 @@ from dataclasses import fields -from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO from logging import warning -from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState +from typing import Any, TypedDict, ClassVar, TextIO + +from BaseClasses import Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState +from Options import PlandoConnection, OptionError, PerGameCommonOptions, Range, Removed +from settings import Group, Bool, FilePath +from worlds.AutoWorld import WebWorld, World + +# from .bells import bell_location_groups, bell_location_name_to_id +from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table +from .combat_logic import area_data, CombatState +from .er_data import portal_mapping, RegionInfo, tunic_er_regions +from .er_rules import set_er_location_rules +from .er_scripts import create_er_regions, verify_plando_directions +# from .fuses import fuse_location_name_to_id, fuse_location_groups +from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, combat_items) from .locations import location_table, location_name_groups, standard_location_name_to_id, hexagon_locations -from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon -from .er_rules import set_er_location_rules -from .regions import tunic_regions -from .er_scripts import create_er_regions, verify_plando_directions -from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations -from .er_data import portal_mapping, RegionInfo, tunic_er_regions +from .logic_helpers import randomize_ability_unlocks, gold_hexagon from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, - LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options, - get_hexagons_in_pool, HexagonQuestAbilityUnlockType, EntranceLayout) -from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table -from .combat_logic import area_data, CombatState + LaurelsLocation, LaurelsZips, IceGrappling, LadderStorage, EntranceLayout, + check_options, LocalFill, get_hexagons_in_pool, HexagonQuestAbilityUnlockType) from . import ut_stuff -from worlds.AutoWorld import WebWorld, World -from Options import PlandoConnection, OptionError, PerGameCommonOptions, Removed, Range -from settings import Group, Bool, FilePath class TunicSettings(Group): @@ -28,15 +31,15 @@ class TunicSettings(Group): class LimitGrassRando(Bool): """Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95.""" - + class UTPoptrackerPath(FilePath): """Path to the user's TUNIC Poptracker Pack.""" description = "TUNIC Poptracker Pack zip file" required = False - disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False - limit_grass_rando: Union[LimitGrassRando, bool] = True - ut_poptracker_path: Union[UTPoptrackerPath, str] = UTPoptrackerPath() + disable_local_spoiler: DisableLocalSpoiler | bool = False + limit_grass_rando: LimitGrassRando | bool = True + ut_poptracker_path: UTPoptrackerPath | str = UTPoptrackerPath() class TunicWeb(WebWorld): @@ -68,10 +71,10 @@ class SeedGroup(TypedDict): laurels_zips: bool # laurels_zips value ice_grappling: int # ice_grappling value ladder_storage: int # ls value - laurels_at_10_fairies: bool # laurels location value + laurels_at_10_fairies: bool # whether laurels location is set to 10 fairies entrance_layout: int # entrance layout value has_decoupled_enabled: bool # for checking that players don't have conflicting options - plando: List[PlandoConnection] # consolidated plando connections for the seed group + plando: list[PlandoConnection] # consolidated plando connections for the seed group class TunicWorld(World): @@ -82,54 +85,66 @@ class TunicWorld(World): """ game = "TUNIC" web = TunicWeb() + author: str = "SilentSR & ScipioWright" options: TunicOptions options_dataclass = TunicOptions settings: ClassVar[TunicSettings] item_name_groups = item_name_groups + # grass, breakables, fuses, and bells are separated out into their own files + # this makes for easier organization, at the cost of stuff like what's directly below here location_name_groups = location_name_groups for group_name, members in grass_location_name_groups.items(): location_name_groups.setdefault(group_name, set()).update(members) for group_name, members in breakable_location_groups.items(): location_name_groups.setdefault(group_name, set()).update(members) + # for group_name, members in fuse_location_groups.items(): + # location_name_groups.setdefault(group_name, set()).update(members) + # for group_name, members in bell_location_groups.items(): + # location_name_groups.setdefault(group_name, set()).update(members) item_name_to_id = item_name_to_id location_name_to_id = standard_location_name_to_id.copy() location_name_to_id.update(grass_location_name_to_id) location_name_to_id.update(breakable_location_name_to_id) + # location_name_to_id.update(fuse_location_name_to_id) + # location_name_to_id.update(bell_location_name_to_id) - player_location_table: Dict[str, int] - ability_unlocks: Dict[str, int] - slot_data_items: List[TunicItem] - tunic_portal_pairs: Dict[str, str] - er_portal_hints: Dict[int, str] - seed_groups: Dict[str, SeedGroup] = {} - used_shop_numbers: Set[int] - er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work + player_location_table: dict[str, int] + ability_unlocks: dict[str, int] + slot_data_items: list[TunicItem] + tunic_portal_pairs: dict[str, str] + er_portal_hints: dict[int, str] + seed_groups: dict[str, SeedGroup] = {} + used_shop_numbers: set[int] + er_regions: dict[str, RegionInfo] # absolutely needed so outlet regions work # for the local_fill option - fill_items: List[TunicItem] - fill_locations: List[Location] + fill_items: list[TunicItem] + fill_locations: list[Location] + backup_locations: list[Location] amount_to_local_fill: int # so we only loop the multiworld locations once # if these are locations instead of their info, it gives a memory leak error - item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {} - player_item_link_locations: Dict[str, List[Location]] + item_link_locations: dict[int, dict[str, list[tuple[int, str]]]] = {} + player_item_link_locations: dict[str, list[Location]] using_ut: bool # so we can check if we're using UT only once - passthrough: Dict[str, Any] + passthrough: dict[str, Any] ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml tracker_world: ClassVar = ut_stuff.tracker_world def generate_early(self) -> None: + # if you have multiple APWorlds, we want it to fail here instead of at the end of gen try: int(self.settings.disable_local_spoiler) except AttributeError: raise Exception("You have a TUNIC APWorld in your lib/worlds folder and custom_worlds folder.\n" "This would cause an error at the end of generation.\n" "Please remove one of them, most likely the one in lib/worlds.") - + + # hidden option for me to do multi-slot test gens with random options more easily if self.options.all_random: for option_name in (attr.name for attr in fields(TunicOptions) if attr not in fields(PerGameCommonOptions)): @@ -145,10 +160,11 @@ class TunicWorld(World): option.value = self.random.choice(list(option.name_lookup)) check_options(self) - self.er_regions = tunic_er_regions.copy() + # empty plando connections if ER is off if self.options.plando_connections and not self.options.entrance_rando: self.options.plando_connections.value = () + # modify direction and order of plando connections for more consistency later on if self.options.plando_connections: def replace_connection(old_cxn: PlandoConnection, new_cxn: PlandoConnection, index: int) -> None: self.options.plando_connections.value.remove(old_cxn) @@ -180,6 +196,7 @@ class TunicWorld(World): self.player_location_table = standard_location_name_to_id.copy() + # setup our defaults for the local_fill option if self.options.local_fill == -1: if self.options.grass_randomizer: if self.options.breakable_shuffle: @@ -206,9 +223,15 @@ class TunicWorld(World): self.player_location_table.update({name: num for name, num in breakable_location_name_to_id.items() if not name.startswith("Purgatory")}) + # if self.options.shuffle_fuses: + # self.player_location_table.update(fuse_location_name_to_id) + # + # if self.options.shuffle_bells: + # self.player_location_table.update(bell_location_name_to_id) + @classmethod def stage_generate_early(cls, multiworld: MultiWorld) -> None: - tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") + tunic_worlds: tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") for tunic in tunic_worlds: # setting up state combat logic stuff, see has_combat_reqs for its use # and this is magic so pycharm doesn't like it, unfortunately @@ -314,10 +337,10 @@ class TunicWorld(World): return TunicItem(name, itemclass, self.item_name_to_id[name], self.player) def create_items(self) -> None: - tunic_items: List[TunicItem] = [] + tunic_items: list[TunicItem] = [] self.slot_data_items = [] - items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} + items_to_create: dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} # Calculate number of hexagons in item pool if self.options.hexagon_quest: @@ -377,7 +400,7 @@ class TunicWorld(World): items_to_create[rgb_hexagon] = 0 # Filler items in the item pool - available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and + available_filler: list[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and item_table[filler].classification == ItemClassification.filler] # Remove filler to make room for other items @@ -457,8 +480,8 @@ class TunicWorld(World): # discard grass from non_local if it's meant to be limited if self.settings.limit_grass_rando: self.options.non_local_items.value.discard("Grass") - all_filler: List[TunicItem] = [] - non_filler: List[TunicItem] = [] + all_filler: list[TunicItem] = [] + non_filler: list[TunicItem] = [] for tunic_item in tunic_items: if (tunic_item.excludable and tunic_item.name not in self.options.local_items @@ -477,7 +500,7 @@ class TunicWorld(World): if self.options.local_fill > 0 and self.multiworld.players > 1: # we need to reserve a couple locations so that we don't fill up every sphere 1 location sphere_one_locs = self.multiworld.get_reachable_locations(CollectionState(self.multiworld), self.player) - reserved_locations: Set[Location] = set(self.random.sample(sphere_one_locs, 2)) + reserved_locations: set[Location] = set(self.random.sample(sphere_one_locs, 2)) viable_locations = [loc for loc in self.multiworld.get_unfilled_locations(self.player) if loc not in reserved_locations and loc.name not in self.options.priority_locations.value] @@ -487,34 +510,91 @@ class TunicWorld(World): f"This is likely due to excess plando or priority locations.") self.random.shuffle(viable_locations) self.fill_locations = viable_locations[:self.amount_to_local_fill] + self.backup_locations = viable_locations[self.amount_to_local_fill:] @classmethod def stage_pre_fill(cls, multiworld: MultiWorld) -> None: - tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") + tunic_fill_worlds: list[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") if world.options.local_fill.value > 0] if tunic_fill_worlds and multiworld.players > 1: - grass_fill: List[TunicItem] = [] - non_grass_fill: List[TunicItem] = [] - grass_fill_locations: List[Location] = [] - non_grass_fill_locations: List[Location] = [] + grass_fill: list[TunicItem] = [] + non_grass_fill: list[TunicItem] = [] + grass_fill_locations: list[Location] = [] + non_grass_fill_locations: list[Location] = [] + backup_grass_locations: list[Location] = [] + backup_non_grass_locations: list[Location] = [] for world in tunic_fill_worlds: if world.options.grass_randomizer: grass_fill.extend(world.fill_items) grass_fill_locations.extend(world.fill_locations) + backup_grass_locations.extend(world.backup_locations) else: non_grass_fill.extend(world.fill_items) non_grass_fill_locations.extend(world.fill_locations) + backup_non_grass_locations.extend(world.backup_locations) multiworld.random.shuffle(grass_fill) multiworld.random.shuffle(non_grass_fill) multiworld.random.shuffle(grass_fill_locations) multiworld.random.shuffle(non_grass_fill_locations) + multiworld.random.shuffle(backup_grass_locations) + multiworld.random.shuffle(backup_non_grass_locations) + + # these are slots that filled in TUNIC locations during pre_fill + out_of_spec_worlds = set() for filler_item in grass_fill: - grass_fill_locations.pop().place_locked_item(filler_item) + loc_to_fill = grass_fill_locations.pop() + try: + loc_to_fill.place_locked_item(filler_item) + except Exception: + out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game) + for loc in backup_grass_locations: + if not loc.item: + loc.place_locked_item(filler_item) + break + else: + out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game) + else: + raise Exception("TUNIC: Could not fulfill local_filler option. This issue is caused by another " + "world filling TUNIC locations during pre_fill.\n" + "Archipelago does not allow us to place items into the item pool after " + "create_items, so we cannot recover from this issue.\n" + f"This is likely caused by the following world(s): {out_of_spec_worlds}.\n" + f"Please let the world dev(s) for the listed world(s) know that there is an " + f"issue there.\n" + "As a workaround, you can try setting the local_filler option lower for " + "TUNIC slots with Breakable Shuffle or Grass Rando enabled. You may be able to " + "try generating again, as it may not happen every generation.") for filler_item in non_grass_fill: - non_grass_fill_locations.pop().place_locked_item(filler_item) + loc_to_fill = non_grass_fill_locations.pop() + try: + loc_to_fill.place_locked_item(filler_item) + except Exception: + out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game) + for loc in backup_non_grass_locations: + if not loc.item: + loc.place_locked_item(filler_item) + break + else: + out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game) + else: + raise Exception("TUNIC: Could not fulfill local_filler option. This issue is caused by another " + "world filling TUNIC locations during pre_fill.\n" + "Archipelago does not allow us to place items into the item pool after " + "create_items, so we cannot recover from this issue.\n" + f"This is likely caused by the following world(s): {out_of_spec_worlds}.\n" + f"Please let the world dev(s) for the listed world(s) know that there is an " + f"issue there.\n" + "As a workaround, you can try setting the local_filler option lower for " + "TUNIC slots with Breakable Shuffle or Grass Rando enabled. You may be able to " + "try generating again, as it may not happen every generation.") + if out_of_spec_worlds: + warning("TUNIC: At least one other world has filled TUNIC locations during pre_fill. This may " + "cause issues for games that rely on placing items in their own world during pre_fill.\n" + f"This is likely being caused by the following world(s): {out_of_spec_worlds}.\n" + "Please let the world dev(s) for the listed world(s) know that there is an issue there.") def create_regions(self) -> None: self.tunic_portal_pairs = {} @@ -522,48 +602,19 @@ class TunicWorld(World): self.ability_unlocks = randomize_ability_unlocks(self) # stuff for universal tracker support, can be ignored for standard gen - if self.using_ut: + if self.using_ut and self.options.hexagon_quest_ability_type == "hexagons": self.ability_unlocks["Pages 24-25 (Prayer)"] = self.passthrough["Hexagon Quest Prayer"] self.ability_unlocks["Pages 42-43 (Holy Cross)"] = self.passthrough["Hexagon Quest Holy Cross"] self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"] - # Most non-standard options use ER regions - if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic - or self.options.grass_randomizer or self.options.breakable_shuffle): - portal_pairs = create_er_regions(self) - if self.options.entrance_rando: - # these get interpreted by the game to tell it which entrances to connect - for portal1, portal2 in portal_pairs.items(): - self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() - else: - # uses the original rules, easier to navigate and reference - for region_name in tunic_regions: - region = Region(region_name, self.player, self.multiworld) - self.multiworld.regions.append(region) - - for region_name, exits in tunic_regions.items(): - region = self.get_region(region_name) - region.add_exits(exits) - - for location_name, location_id in self.player_location_table.items(): - region = self.get_region(location_table[location_name].region) - location = TunicLocation(self.player, location_name, location_id, region) - region.locations.append(location) - - victory_region = self.get_region("Spirit Arena") - victory_location = TunicLocation(self.player, "The Heir", None, victory_region) - victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) - self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - victory_region.locations.append(victory_location) + portal_pairs = create_er_regions(self) + if self.options.entrance_rando: + # these get interpreted by the game to tell it which entrances to connect + for portal1, portal2 in portal_pairs.items(): + self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() def set_rules(self) -> None: - # same reason as in create_regions - if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic - or self.options.grass_randomizer or self.options.breakable_shuffle): - set_er_location_rules(self) - else: - set_region_rules(self) - set_location_rules(self) + set_er_location_rules(self) def get_filler_item_name(self) -> str: return self.random.choice(filler_items) @@ -582,14 +633,14 @@ class TunicWorld(World): return change def write_spoiler_header(self, spoiler_handle: TextIO): - if self.options.hexagon_quest and self.options.ability_shuffling\ - and self.options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: + if (self.options.hexagon_quest and self.options.ability_shuffling + and self.options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons): spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n") for ability in self.ability_unlocks: # Remove parentheses for better readability spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n') - def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: + def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) # all state seems to have efficient paths @@ -626,7 +677,7 @@ class TunicWorld(World): if hint_text: hint_data[self.player][location.address] = hint_text - def get_real_location(self, location: Location) -> Tuple[str, int]: + def get_real_location(self, location: Location) -> tuple[str, int]: # if it's not in a group, it's not in an item link if location.player not in self.multiworld.groups or not location.item: return location.name, location.player @@ -638,8 +689,8 @@ class TunicWorld(World): f"Using a potentially incorrect location name instead.") return location.name, location.player - def fill_slot_data(self) -> Dict[str, Any]: - slot_data: Dict[str, Any] = { + def fill_slot_data(self) -> dict[str, Any]: + slot_data: dict[str, Any] = { "seed": self.random.randint(0, 2147483647), "start_with_sword": self.options.start_with_sword.value, "keys_behind_bosses": self.options.keys_behind_bosses.value, @@ -657,6 +708,8 @@ class TunicWorld(World): "entrance_rando": int(bool(self.options.entrance_rando.value)), "decoupled": self.options.decoupled.value if self.options.entrance_rando else 0, "shuffle_ladders": self.options.shuffle_ladders.value, + # "shuffle_fuses": self.options.shuffle_fuses.value, + # "shuffle_bells": self.options.shuffle_bells.value, "grass_randomizer": self.options.grass_randomizer.value, "combat_logic": self.options.combat_logic.value, "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], @@ -674,7 +727,7 @@ class TunicWorld(World): # checking if groups so that this doesn't run if the player isn't in a group if groups: if not self.item_link_locations: - tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") + tunic_worlds: tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") # figure out our groups and the items in them for tunic in tunic_worlds: for group in self.multiworld.get_player_groups(tunic.player): @@ -710,7 +763,7 @@ class TunicWorld(World): # for the universal tracker, doesn't get called in standard gen # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md @staticmethod - def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: + def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]: # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough # we are using re_gen_passthrough over modifying the world here due to complexities with ER return slot_data diff --git a/worlds/tunic/breakables.py b/worlds/tunic/breakables.py index 156bece7..b9af437c 100644 --- a/worlds/tunic/breakables.py +++ b/worlds/tunic/breakables.py @@ -1,18 +1,18 @@ +from enum import IntEnum from typing import TYPE_CHECKING, NamedTuple -from enum import IntEnum from BaseClasses import CollectionState, Region from worlds.generic.Rules import set_rule -from .rules import has_sword, has_melee + +from .constants import base_id from .er_rules import can_shop +from .logic_helpers import has_sword, has_melee + + if TYPE_CHECKING: from . import TunicWorld -# just getting an id that is a decent chunk ahead of the grass ones -breakable_base_id = 509342400 + 8000 - - class BreakableType(IntEnum): pot = 1 fire_pot = 2 @@ -341,6 +341,7 @@ breakable_location_table: dict[str, TunicLocationData] = { } +breakable_base_id = base_id + 8000 breakable_location_name_to_id: dict[str, int] = {name: breakable_base_id + index for index, name in enumerate(breakable_location_table)} @@ -358,6 +359,7 @@ loc_group_convert: dict[str, str] = { "Beneath the Well Main": "Beneath the Well", "Well Boss": "Dark Tomb Checkpoint", "Dark Tomb Main": "Dark Tomb", + "Magic Dagger House": "West Garden House", "Fortress Courtyard Upper": "Fortress Courtyard", "Fortress Courtyard Upper pot": "Fortress Courtyard", "Fortress Courtyard west pots": "Fortress Courtyard", @@ -370,13 +372,16 @@ loc_group_convert: dict[str, str] = { "Fortress Grave Path westmost pot": "Fortress Grave Path", "Fortress Grave Path pots": "Fortress Grave Path", "Dusty": "Fortress Leaf Piles", - "Frog Stairs Upper": "Frog Stairs", + "Frog Stairs Upper": "Frog Stairway", + "Frog's Domain Front": "Frog's Domain", + "Frog's Domain Main": "Frog's Domain", "Quarry Monastery Entry": "Quarry", "Quarry Back": "Quarry", "Lower Quarry": "Quarry", "Lower Quarry upper pots": "Quarry", "Even Lower Quarry": "Quarry", "Monastery Back": "Monastery", + "Cathedral Main": "Cathedral", } diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py index dbf1e864..d12450fb 100644 --- a/worlds/tunic/combat_logic.py +++ b/worlds/tunic/combat_logic.py @@ -1,10 +1,12 @@ -from typing import Dict, List, NamedTuple, Tuple, Optional -from enum import IntEnum from collections import defaultdict +from enum import IntEnum +from typing import NamedTuple + from BaseClasses import CollectionState -from .rules import has_sword, has_melee from worlds.AutoWorld import LogicMixin +from .logic_helpers import has_sword, has_melee + # the vanilla stats you are expected to have to get through an area, based on where they are in vanilla class AreaStats(NamedTuple): @@ -16,12 +18,12 @@ class AreaStats(NamedTuple): sp_level: int mp_level: int potion_count: int - equipment: List[str] = [] + equipment: list[str] = [] is_boss: bool = False # the vanilla upgrades/equipment you would have -area_data: Dict[str, AreaStats] = { +area_data: dict[str, AreaStats] = { # The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something "Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]), "East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]), @@ -52,9 +54,9 @@ area_data: Dict[str, AreaStats] = { # these are used for caching which areas can currently be reached in state # Gauntlet does not have exclusively higher stat requirements, so it will be checked separately -boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] +boss_areas: list[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] # Swamp does not have exclusively higher stat requirements, so it will be checked separately -non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"] +non_boss_areas: list[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"] class CombatState(IntEnum): @@ -114,7 +116,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool return met_combat_reqs -def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: +def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: AreaStats | None = None) -> bool: data = alt_data or area_data[area_name] extra_att_needed = 0 extra_def_needed = 0 @@ -303,7 +305,7 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> # returns a tuple of your max attack level, the number of attack offerings -def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_att_level(state: CollectionState, player: int) -> tuple[int, int]: att_offerings = state.count("ATT Offering", player) att_upgrades = state.count("Hero Relic - ATT", player) sword_level = state.count("Sword Upgrade", player) @@ -315,44 +317,44 @@ def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]: # returns a tuple of your max defense level, the number of defense offerings -def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_def_level(state: CollectionState, player: int) -> tuple[int, int]: def_offerings = state.count("DEF Offering", player) # defense falls off, can just cap it at 8 for simplicity return (min(8, 1 + def_offerings - + state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)) + + state.count_from_list(("Hero Relic - DEF", "Secret Legend", "Phonomath"), player)) + (2 if state.has("Shield", player) else 0) + (2 if state.has("Hero's Laurels", player) else 0), def_offerings) # returns a tuple of your max potion level, the number of potion offerings -def get_potion_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_potion_level(state: CollectionState, player: int) -> tuple[int, int]: potion_offerings = min(2, state.count("Potion Offering", player)) # your third potion upgrade (from offerings) costs 1,000 money, reasonable to assume you won't do that return (1 + potion_offerings - + state.count_from_list({"Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"}, player), + + state.count_from_list(("Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"), player), potion_offerings) # returns a tuple of your max hp level, the number of hp offerings -def get_hp_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_hp_level(state: CollectionState, player: int) -> tuple[int, int]: hp_offerings = state.count("HP Offering", player) return 1 + hp_offerings + state.count("Hero Relic - HP", player), hp_offerings # returns a tuple of your max sp level, the number of sp offerings -def get_sp_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_sp_level(state: CollectionState, player: int) -> tuple[int, int]: sp_offerings = state.count("SP Offering", player) return (1 + sp_offerings - + state.count_from_list({"Hero Relic - SP", "Mr Mayor", "Power Up", - "Regal Weasel", "Forever Friend"}, player), + + state.count_from_list(("Hero Relic - SP", "Mr Mayor", "Power Up", + "Regal Weasel", "Forever Friend"), player), sp_offerings) -def get_mp_level(state: CollectionState, player: int) -> Tuple[int, int]: +def get_mp_level(state: CollectionState, player: int) -> tuple[int, int]: mp_offerings = state.count("MP Offering", player) return (1 + mp_offerings - + state.count_from_list({"Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"}, player), + + state.count_from_list(("Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"), player), mp_offerings) @@ -426,9 +428,9 @@ def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int: class TunicState(LogicMixin): - tunic_need_to_reset_combat_from_collect: Dict[int, bool] - tunic_need_to_reset_combat_from_remove: Dict[int, bool] - tunic_area_combat_state: Dict[int, Dict[str, int]] + tunic_need_to_reset_combat_from_collect: dict[int, bool] + tunic_need_to_reset_combat_from_remove: dict[int, bool] + tunic_area_combat_state: dict[int, dict[str, int]] def init_mixin(self, _): # the per-player need to reset the combat state when collecting a combat item diff --git a/worlds/tunic/constants.py b/worlds/tunic/constants.py new file mode 100644 index 00000000..2543fd3c --- /dev/null +++ b/worlds/tunic/constants.py @@ -0,0 +1,51 @@ +base_id = 509342400 + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +gun = "Gun" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +icebolt = "Pages 52-53 (Icebolt)" +shield = "Shield" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + +swamp_fuse_1 = "Swamp Fuse 1" +swamp_fuse_2 = "Swamp Fuse 2" +swamp_fuse_3 = "Swamp Fuse 3" +cathedral_elevator_fuse = "Cathedral Elevator Fuse" +quarry_fuse_1 = "Quarry Fuse 1" +quarry_fuse_2 = "Quarry Fuse 2" +ziggurat_miniboss_fuse = "Ziggurat Miniboss Fuse" +ziggurat_teleporter_fuse = "Ziggurat Teleporter Fuse" +fortress_exterior_fuse_1 = "Fortress Exterior Fuse 1" +fortress_exterior_fuse_2 = "Fortress Exterior Fuse 2" +fortress_courtyard_upper_fuse = "Fortress Courtyard Upper Fuse" +fortress_courtyard_lower_fuse = "Fortress Courtyard Fuse" +beneath_the_vault_fuse = "Beneath the Vault Fuse" # event needs to be renamed probably +fortress_candles_fuse = "Fortress Candles Fuse" +fortress_door_left_fuse = "Fortress Door Left Fuse" +fortress_door_right_fuse = "Fortress Door Right Fuse" +west_furnace_fuse = "West Furnace Fuse" +west_garden_fuse = "West Garden Fuse" +atoll_northeast_fuse = "Atoll Northeast Fuse" +atoll_northwest_fuse = "Atoll Northwest Fuse" +atoll_southeast_fuse = "Atoll Southeast Fuse" +atoll_southwest_fuse = "Atoll Southwest Fuse" +library_lab_fuse = "Library Lab Fuse" + +# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules +bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", + "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", + "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"] diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 744326aa..cfa215a3 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,5 +1,5 @@ -from typing import Dict, NamedTuple, List, Optional, TYPE_CHECKING from enum import IntEnum +from typing import NamedTuple, TYPE_CHECKING if TYPE_CHECKING: from . import TunicWorld @@ -36,7 +36,7 @@ class Portal(NamedTuple): return self.destination + ", " + self.scene() + self.tag -portal_mapping: List[Portal] = [ +portal_mapping: list[Portal] = [ Portal(name="Stick House Entrance", region="Overworld", destination="Sword Cave", tag="_", direction=Direction.north), Portal(name="Windmill Entrance", region="Overworld", @@ -535,7 +535,7 @@ portal_mapping: List[Portal] = [ class RegionInfo(NamedTuple): game_scene: str # the name of the scene in the actual game dead_end: int = 0 # if a region has only one exit - outlet_region: Optional[str] = None + outlet_region: str | None = None is_fake_region: bool = False @@ -553,7 +553,7 @@ class DeadEnd(IntEnum): # key is the AP region name. "Fake" in region info just means the mod won't receive that info at all -tunic_er_regions: Dict[str, RegionInfo] = { +tunic_er_regions: dict[str, RegionInfo] = { "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True), "Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True), # main overworld holy cross checks @@ -735,6 +735,7 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Rooted Ziggurat Lower Entry": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic "Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin + "Rooted Ziggurat Lower Miniboss Platform": RegionInfo("ziggurat2020_3"), # the double admin platform "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry", is_fake_region=True), # for use with fixed shop on "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side @@ -775,7 +776,6 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, is_fake_region=True), } - # this is essentially a pared down version of the region connections in rules.py, with some minor differences # the main purpose of this is to make it so that you can access every region # most items are excluded from the rules here, since we can assume Archipelago will properly place them @@ -786,7 +786,7 @@ tunic_er_regions: Dict[str, RegionInfo] = { # LS# refers to ladder storage difficulties # LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic # this is because it just means you can reach the entrances in that region via ladder storage -traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { +traversal_requirements: dict[str, dict[str, list[list[str]]]] = { "Overworld": { "Overworld Beach": [], @@ -801,7 +801,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld Swamp Lower Entry": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["LS1"]], + [["LS1"]], "Overworld Well Entry Area": [], "Overworld Ruined Passage Door": @@ -823,7 +823,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld Tunnel Turret": [["IG1"], ["LS1"], ["Hyperdash"]], "Overworld Temple Door": - [["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]], + [["Bell Shuffle"], ["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]], "Overworld Southeast Cross Door": [], "Overworld Fountain Cross Door": @@ -1229,7 +1229,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "West Garden by Portal": { "West Garden Portal": - [["West Garden South Checkpoint"]], + [["Fuse Shuffle"], ["West Garden South Checkpoint"]], "West Garden Portal Item": [["Hyperdash"]], }, @@ -1468,7 +1468,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Eastern Vault Fortress": { "Eastern Vault Fortress Gold Door": - [["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], + [["IG2"], ["Fuse Shuffle"], + ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], }, "Eastern Vault Fortress Gold Door": { "Eastern Vault Fortress": @@ -1514,7 +1515,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Fortress Arena": { "Fortress Arena Portal": - [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], + [["Fuse Shuffle"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], }, "Fortress Arena Portal": { "Fortress Arena": @@ -1547,7 +1548,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Quarry Entry": { "Quarry Portal": - [["Quarry Connector"]], + [["Fuse Shuffle"], ["Quarry Connector"]], "Quarry": [], "Monastery Rope": @@ -1593,7 +1594,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Even Lower Quarry": [], "Lower Quarry Zig Door": - [["Quarry", "Quarry Connector"], ["IG3"]], + [["Fuse Shuffle"], ["Quarry", "Quarry Connector"], ["IG3"]], }, "Monastery Rope": { "Quarry Back": @@ -1636,13 +1637,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [["Hyperdash"]], "Rooted Ziggurat Lower Front": [], - "Rooted Ziggurat Lower Back": + "Rooted Ziggurat Lower Miniboss Platform": [], }, + "Rooted Ziggurat Lower Miniboss Platform": { + "Rooted Ziggurat Lower Mid Checkpoint": + [], + "Rooted Ziggurat Lower Back": + [] + }, "Rooted Ziggurat Lower Back": { "Rooted Ziggurat Lower Entry": [["LS2"]], - "Rooted Ziggurat Lower Mid Checkpoint": + "Rooted Ziggurat Lower Miniboss Platform": [["Hyperdash"], ["IG1"]], "Rooted Ziggurat Portal Room Entrance": [], @@ -1658,7 +1665,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Rooted Ziggurat Portal Room": { "Rooted Ziggurat Portal Room Exit": - [["Rooted Ziggurat Lower Back"]], + [["Fuse Shuffle"], ["Rooted Ziggurat Lower Back"]], "Rooted Ziggurat Portal": [], }, @@ -1742,7 +1749,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Cathedral Main": [], }, - "Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet": [], @@ -1762,13 +1768,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Far Shore to East Forest Region": [["Hyperdash"]], "Far Shore to Quarry Region": - [["Quarry Connector", "Quarry"]], + [["Fuse Shuffle"], ["Quarry Connector", "Quarry"]], "Far Shore to Library Region": - [["Library Lab"]], + [["Fuse Shuffle"], ["Library Lab"]], "Far Shore to West Garden Region": - [["West Garden South Checkpoint"]], + [["Fuse Shuffle"], ["West Garden South Checkpoint"]], "Far Shore to Fortress Region": - [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], + [["Fuse Shuffle"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], }, "Far Shore to Spawn Region": { "Far Shore": diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index edd6021c..6d238693 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,58 +1,32 @@ -from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING +from typing import FrozenSet, TYPE_CHECKING + +from BaseClasses import Region from worlds.generic.Rules import set_rule, add_rule, forbid_item -from BaseClasses import Region, CollectionState -from .options import IceGrappling, LadderStorage, CombatLogic -from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, - laurels_zip, bomb_walls) -from .er_data import Portal, get_portal_outlet_region -from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls + +# from .bells import set_bell_location_rules from .combat_logic import has_combat_reqs +from .constants import * +from .er_data import Portal, get_portal_outlet_region +# from .fuses import set_fuse_location_rules, has_fuses from .grass import set_grass_location_rules +from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls +from .logic_helpers import (has_ability, has_ladder, has_melee, has_sword, has_lantern, has_mask, has_fuses, + can_shop, can_get_past_bushes, laurels_zip, has_ice_grapple_logic, can_ladder_storage) +from .options import IceGrappling, LadderStorage, CombatLogic if TYPE_CHECKING: from . import TunicWorld -laurels = "Hero's Laurels" -grapple = "Magic Orb" -ice_dagger = "Magic Dagger" -fire_wand = "Magic Wand" -gun = "Gun" -lantern = "Lantern" -fairies = "Fairy" -coins = "Golden Coin" -prayer = "Pages 24-25 (Prayer)" -holy_cross = "Pages 42-43 (Holy Cross)" -icebolt = "Pages 52-53 (Icebolt)" -key = "Key" -house_key = "Old House Key" -vault_key = "Fortress Vault Key" -mask = "Scavenger Mask" -red_hexagon = "Red Questagon" -green_hexagon = "Green Questagon" -blue_hexagon = "Blue Questagon" -gold_hexagon = "Gold Questagon" +fuses_option = False # replace with options.shuffle_fuses when fuse shuffle is in +bells_option = False # replace with options.shuffle_bells when bell shuffle is in -def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: - return not world.options.shuffle_ladders or state.has(ladder, world.player) - - -def can_shop(state: CollectionState, world: "TunicWorld") -> bool: - return has_sword(state, world.player) and state.can_reach_region("Shop", world.player) - - -# for the ones that are not early bushes where ER can screw you over a bit -def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool: - # add in glass cannon + stick for grass rando - return has_sword(state, world.player) or state.has_any((fire_wand, laurels, gun), world.player) - - -def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: +def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_pairs: dict[Portal, Portal]) -> None: player = world.player options = world.options # input scene destination tag, returns portal's name and paired portal's outlet region or region - def get_portal_info(portal_sd: str) -> Tuple[str, str]: + def get_portal_info(portal_sd: str) -> tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: return portal1.name, get_portal_outlet_region(portal2, world) @@ -61,7 +35,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ raise Exception(f"No matches found in get_portal_info for {portal_sd}") # input scene destination tag, returns paired portal's name and region - def get_paired_portal(portal_sd: str) -> Tuple[str, str]: + def get_paired_portal(portal_sd: str) -> tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: return portal2.name, portal2.region @@ -81,7 +55,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Beach"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) - or state.has_any({laurels, grapple}, player)) + or state.has_any((laurels, grapple), player)) # regions["Overworld Beach"].connect( # connecting_region=regions["Overworld"], # rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) @@ -114,14 +88,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: state.has(laurels, player)) regions["Overworld to Atoll Upper"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has_any({laurels, grapple}, player)) + rule=lambda state: state.has_any((laurels, grapple), player)) regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], rule=lambda state: state.has(laurels, player) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - regions["Overworld Belltower"].connect( - connecting_region=regions["Overworld"]) + # regions["Overworld Belltower"].connect( + # connecting_region=regions["Overworld"]) # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down regions["Overworld Belltower"].connect( @@ -146,17 +120,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Ruined Passage Door"], rule=lambda state: state.has(key, player, 2) or laurels_zip(state, world)) - regions["Overworld Ruined Passage Door"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: laurels_zip(state, world)) + # regions["Overworld Ruined Passage Door"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: laurels_zip(state, world)) regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - regions["After Ruined Passage"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) + # regions["After Ruined Passage"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) # for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down # you can ice grapple through the bomb wall, so no need for shop logic checking @@ -165,10 +139,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - regions["Above Ruined Passage"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or state.has(laurels, player)) + # regions["Above Ruined Passage"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + # or state.has(laurels, player)) regions["After Ruined Passage"].connect( connecting_region=regions["Above Ruined Passage"], @@ -183,8 +157,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or state.has(laurels, player)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) # nmg: ice grapple the slimes, works both ways consistently regions["East Overworld"].connect( @@ -198,9 +171,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - regions["East Overworld"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) + # regions["East Overworld"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld at Patrol Cave"]) @@ -220,9 +193,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) or state.has(grapple, player)) - regions["Overworld above Patrol Cave"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) + # regions["Overworld above Patrol Cave"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -243,10 +216,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Quarry Entrance"], - rule=lambda state: state.has_any({grapple, laurels}, player)) + rule=lambda state: state.has_any((grapple, laurels), player)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: state.has_any({grapple, laurels}, player)) + rule=lambda state: state.has_any((grapple, laurels), player)) # ice grapple push guard captain down the ledge regions["Upper Overworld"].connect( @@ -267,11 +240,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: state.has_any({laurels, grapple, gun}, player) + rule=lambda state: state.has_any((laurels, grapple, gun), player) or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has_any({laurels, grapple, gun}, player) + rule=lambda state: state.has_any((laurels, grapple, gun), player) or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( @@ -285,24 +258,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - regions["Overworld Quarry Entry"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + # regions["Overworld Quarry Entry"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], rule=lambda state: state.has(laurels, player)) - regions["Overworld Swamp Upper Entry"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: state.has(laurels, player)) + # regions["Overworld Swamp Upper Entry"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: state.has(laurels, player)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], rule=lambda state: has_ladder("Ladder to Swamp", state, world) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - regions["Overworld Swamp Lower Entry"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladder to Swamp", state, world)) + # regions["Overworld Swamp Lower Entry"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld Special Shop Entry"], @@ -335,33 +308,34 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Southeast Cross Door"], rule=lambda state: has_ability(holy_cross, state, world) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - regions["Overworld Southeast Cross Door"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ability(holy_cross, state, world)) + # regions["Overworld Southeast Cross Door"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ability(holy_cross, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Fountain Cross Door"], rule=lambda state: has_ability(holy_cross, state, world) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - regions["Overworld Fountain Cross Door"].connect( - connecting_region=regions["Overworld"]) + # regions["Overworld Fountain Cross Door"].connect( + # connecting_region=regions["Overworld"]) ow_to_town_portal = regions["Overworld"].connect( connecting_region=regions["Overworld Town Portal"], rule=lambda state: has_ability(prayer, state, world)) - regions["Overworld Town Portal"].connect( - connecting_region=regions["Overworld"]) + # regions["Overworld Town Portal"].connect( + # connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Spawn Portal"], rule=lambda state: has_ability(prayer, state, world)) - regions["Overworld Spawn Portal"].connect( - connecting_region=regions["Overworld"]) + # regions["Overworld Spawn Portal"].connect( + # connecting_region=regions["Overworld"]) # nmg: ice grapple through temple door regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], - rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) + rule=lambda state: (state.has_all(("Ring Eastern Bell", "Ring Western Bell"), player) and not bells_option) + or (state.has_all(("East Bell", "West Bell"), player) and bells_option) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( @@ -637,7 +611,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden by Portal"]) regions["West Garden by Portal"].connect( connecting_region=regions["West Garden Portal"], - rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player)) + rule=lambda state: has_ability(prayer, state, world) + and has_fuses("Activate West Garden Fuse", state, world)) regions["West Garden by Portal"].connect( connecting_region=regions["West Garden Portal Item"], @@ -691,12 +666,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ atoll_statue = regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], rule=lambda state: has_ability(prayer, state, world) - and ((has_ladder("Ladders in South Atoll", state, world) - and state.has_any((laurels, grapple), player) - and (has_sword(state, player) or state.has_any((fire_wand, gun), player))) - # shoot fuse and have the shot hit you mid-LS - or (can_ladder_storage(state, world) and state.has(fire_wand, player) - and options.ladder_storage >= LadderStorage.option_hard))) + and (((((has_ladder("Ladders in South Atoll", state, world) + and state.has_any((laurels, grapple), player) + and (has_sword(state, player) or state.has_any((gun, fire_wand), player))) + # shoot fuse and have the shot hit you mid-LS + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard))) and not fuses_option) + or (state.has_all((atoll_northwest_fuse, atoll_northeast_fuse, atoll_southwest_fuse, atoll_southeast_fuse), player) + and fuses_option)) + ) + regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) @@ -742,7 +721,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Library Exterior by Tree"].connect( connecting_region=regions["Library Exterior Ladder Region"], - rule=lambda state: state.has_any({grapple, laurels}, player) + rule=lambda state: state.has_any((grapple, laurels), player) and has_ladder("Ladders in Library", state, world)) regions["Library Exterior Ladder Region"].connect( connecting_region=regions["Library Exterior by Tree"], @@ -785,7 +764,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Library Lab Lower"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: state.has_any({grapple, laurels}, player) + rule=lambda state: state.has_any((grapple, laurels), player) and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Lab Lower"], @@ -802,7 +781,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Library Lab on Portal Pad"].connect( connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(prayer, state, world)) + rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Library Fuse", state, world)) regions["Library Portal"].connect( connecting_region=regions["Library Lab on Portal Pad"]) @@ -823,10 +802,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Exterior near cave"].connect( connecting_region=regions["Fortress Exterior from Overworld"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player) + and fuses_option)) regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) + rule=lambda state: state.has(laurels, player) + or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player) + if fuses_option else has_ability(prayer, state, world))) # shoot far fire pot, enemy gets aggro'd regions["Fortress Exterior near cave"].connect( @@ -889,12 +872,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], - rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", - "Activate Eastern Vault East Fuse"}, player) + rule=lambda state: (has_fuses("Activate Eastern Vault West Fuses", state, world) + and has_fuses("Activate Eastern Vault East Fuse", state, world)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + or (has_fuses("Activate Eastern Vault West Fuses", state, world) + and has_fuses("Activate Eastern Vault East Fuse", state, world) + and fuses_option)) fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect( connecting_region=regions["Fortress Grave Path Combat"]) @@ -925,7 +911,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], - rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Eastern Vault West Fuses", state, world)) regions["Fortress Arena Portal"].connect( connecting_region=regions["Fortress Arena"]) @@ -939,7 +925,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Quarry Entry"].connect( connecting_region=regions["Quarry Portal"], - rule=lambda state: state.has("Activate Quarry Fuse", player)) + rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Quarry Fuse", state, world)) regions["Quarry Portal"].connect( connecting_region=regions["Quarry Entry"]) @@ -990,7 +976,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Even Lower Quarry Isolated Chest"].connect( connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: state.has("Activate Quarry Fuse", player) + rule=lambda state: has_fuses("Activate Quarry Fuse", state, world) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) # don't need the mask for this either, please don't complain about not needing a mask here, you know what you did @@ -1037,21 +1023,26 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ zig_low_mid_to_front = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( connecting_region=regions["Rooted Ziggurat Lower Front"]) - zig_low_mid_to_back = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( - connecting_region=regions["Rooted Ziggurat Lower Back"], - rule=lambda state: state.has(laurels, player) - or (has_sword(state, player) and has_ability(prayer, state, world))) - # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse - zig_low_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect( + regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( + connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"]) + zig_low_miniboss_to_mid = regions["Rooted Ziggurat Lower Miniboss Platform"].connect( connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], - rule=lambda state: (state.has(laurels, player) - or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - and has_ability(prayer, state, world) - and has_sword(state, player)) + rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + else (has_sword(state, player) and has_ability(prayer, state, world))) + # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse + zig_low_miniboss_to_back = regions["Rooted Ziggurat Lower Miniboss Platform"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"], + rule=lambda state: state.has(laurels, player) or (state.has(ziggurat_miniboss_fuse, player) and fuses_option) + or (has_sword(state, player) and has_ability(prayer, state, world) and not fuses_option)) + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + or (state.has(ziggurat_miniboss_fuse, player) and fuses_option)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], - rule=lambda state: has_ability(prayer, state, world)) + rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world)) regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) @@ -1059,11 +1050,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Portal Room"]) regions["Rooted Ziggurat Portal Room"].connect( connecting_region=regions["Rooted Ziggurat Portal"], - rule=lambda state: has_ability(prayer, state, world)) + rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world) and has_ability(prayer, state, world)) regions["Rooted Ziggurat Portal Room"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], - rule=lambda state: state.has("Activate Ziggurat Fuse", player)) + rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world)) regions["Rooted Ziggurat Portal Room Exit"].connect( connecting_region=regions["Rooted Ziggurat Portal Room"]) @@ -1082,19 +1073,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], rule=lambda state: (has_ability(prayer, state, world) - and (has_sword(state, player)) + and has_sword(state, player) and (state.has(laurels, player) # blam yourself in the face with a wand shot off the fuse or (can_ladder_storage(state, world) and state.has(fire_wand, player) and options.ladder_storage >= LadderStorage.option_hard and (not options.shuffle_ladders - or state.has_any({"Ladders in Overworld Town", + or state.has_any(("Ladders in Overworld Town", "Ladder to Swamp", - "Ladders near Weathervane"}, player) + "Ladders near Weathervane"), player) or (state.has("Ladder to Ruined Atoll", player) and state.can_reach_region("Overworld Beach", player))))) and (not options.combat_logic - or has_combat_reqs("Swamp", state, player))) + or has_combat_reqs("Swamp", state, player)) + and not fuses_option) + or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: @@ -1102,7 +1095,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option)) # grapple push the enemy by the door down, then grapple to it. Really jank regions["Swamp Mid"].connect( @@ -1148,7 +1142,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ cath_entry_to_elev = regions["Cathedral Entry"].connect( connecting_region=regions["Cathedral to Gauntlet"], - rule=lambda state: (has_ability(prayer, state, world) + rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if fuses_option else has_ability(prayer, state, world)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) or options.entrance_rando) # elevator is always there in ER regions["Cathedral to Gauntlet"].connect( @@ -1159,11 +1153,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Cathedral Main"].connect( connecting_region=regions["Cathedral Entry"]) - cath_elev_to_main = regions["Cathedral to Gauntlet"].connect( - connecting_region=regions["Cathedral Main"]) - regions["Cathedral Main"].connect( - connecting_region=regions["Cathedral to Gauntlet"]) - regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1191,25 +1180,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Far Shore"].connect( connecting_region=regions["Far Shore to West Garden Region"], - rule=lambda state: state.has("Activate West Garden Fuse", player)) + rule=lambda state: has_fuses("Activate West Garden Fuse", state, world)) regions["Far Shore to West Garden Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( connecting_region=regions["Far Shore to Quarry Region"], - rule=lambda state: state.has("Activate Quarry Fuse", player)) + rule=lambda state: has_fuses("Activate Quarry Fuse", state, world)) regions["Far Shore to Quarry Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( connecting_region=regions["Far Shore to Fortress Region"], - rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + rule=lambda state: has_fuses("Activate Eastern Vault West Fuses", state, world)) regions["Far Shore to Fortress Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( connecting_region=regions["Far Shore to Library Region"], - rule=lambda state: state.has("Activate Library Fuse", player)) + rule=lambda state: has_fuses("Activate Library Fuse", state, world)) regions["Far Shore to Library Region"].connect( connecting_region=regions["Far Shore"]) @@ -1239,7 +1228,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ non_ow_ls_list.extend(hard_ls) # create the ls elevation regions - ladder_regions: Dict[str, Region] = {} + ladder_regions: dict[str, Region] = {} for name in ow_ladder_groups.keys(): ladder_regions[name] = Region(name, player, world.multiworld) @@ -1409,10 +1398,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ lambda state: has_combat_reqs("Dark Tomb", state, player)) set_rule(wg_before_to_after_terry, - lambda state: state.has_any({laurels, ice_dagger}, player) + lambda state: state.has_any((laurels, ice_dagger), player) or has_combat_reqs("West Garden", state, player)) set_rule(wg_after_to_before_terry, - lambda state: state.has_any({laurels, ice_dagger}, player) + lambda state: state.has_any((laurels, ice_dagger), player) or has_combat_reqs("West Garden", state, player)) set_rule(wg_after_terry_to_west_combat, @@ -1453,25 +1442,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(zig_low_mid_to_front, lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) - set_rule(zig_low_mid_to_back, + set_rule(zig_low_miniboss_to_back, lambda state: state.has(laurels, player) - or (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) - set_rule(zig_low_back_to_mid, - lambda state: (state.has(laurels, player) - or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - and has_ability(prayer, state, world) - and has_combat_reqs("Rooted Ziggurat", state, player)) + or (state.has(ziggurat_miniboss_fuse, player) if fuses_option + else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))) + set_rule(zig_low_miniboss_to_mid, + lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) # only activating the fuse requires combat logic set_rule(cath_entry_to_elev, lambda state: options.entrance_rando or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player))) + or (state.has(cathedral_elevator_fuse, player) if fuses_option + else (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player)))) set_rule(cath_entry_to_main, lambda state: has_combat_reqs("Swamp", state, player)) - set_rule(cath_elev_to_main, - lambda state: has_combat_reqs("Swamp", state, player)) # for spots where you can go into and come out of an entrance to reset enemy aggro if world.options.entrance_rando: @@ -1543,10 +1530,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ def set_er_location_rules(world: "TunicWorld") -> None: player = world.player + options = world.options - if world.options.grass_randomizer: + if options.grass_randomizer: set_grass_location_rules(world) + # if options.shuffle_fuses: + # set_fuse_location_rules(world) + # + # if options.shuffle_bells: + # set_bell_location_rules(world) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules @@ -1557,7 +1551,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), lambda state: has_ability(holy_cross, state, world)) set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), - lambda state: state.has("Activate Furnace Fuse", player)) + lambda state: has_fuses("Activate Furnace Fuse", state, world)) set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), lambda state: has_ability(holy_cross, state, world)) set_rule(world.get_location("Library Hall - Holy Cross Chest"), @@ -1583,9 +1577,9 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Overworld set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), - lambda state: state.has_any({grapple, laurels}, player)) + lambda state: state.has_any((grapple, laurels), player)) set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), - lambda state: state.has_any({grapple, laurels}, player)) + lambda state: state.has_any((grapple, laurels), player)) set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), @@ -1635,9 +1629,9 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) set_rule(world.get_location("East Forest - Lower Dash Chest"), - lambda state: state.has_all({grapple, laurels}, player)) + lambda state: state.has_all((grapple, laurels), player)) set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: ( - state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world))) + state.has_all((grapple, ice_dagger, fire_wand), player) and has_ability(icebolt, state, world))) # Dark Tomb # added to make combat logic smoother @@ -1669,11 +1663,11 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), - lambda state: state.has_any({grapple, laurels}, player)) + lambda state: state.has_any((grapple, laurels), player)) set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), - lambda state: state.has_any({grapple, laurels}, player)) + lambda state: state.has_any((grapple, laurels), player)) set_rule(world.get_location("Frog's Domain - Escape Chest"), - lambda state: state.has_any({grapple, laurels}, player)) + lambda state: state.has_any((grapple, laurels), player)) # Library Lab set_rule(world.get_location("Library Lab - Page 1"), @@ -1695,7 +1689,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), lambda state: has_lantern(state, world) and - (has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))) + (has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))) # Quarry set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), @@ -1706,9 +1700,10 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) - or world.options.entrance_rando))) + or options.entrance_rando))) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), - lambda state: has_sword(state, player) and has_ability(prayer, state, world)) + lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + else has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), @@ -1750,34 +1745,36 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) # Events - set_rule(world.get_location("Eastern Bell"), - lambda state: (has_melee(state, player) or state.has(fire_wand, player))) - set_rule(world.get_location("Western Bell"), - lambda state: (has_melee(state, player) or state.has(fire_wand, player))) - set_rule(world.get_location("Furnace Fuse"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("South and West Fortress Exterior Fuses"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("Beneath the Vault Fuse"), - lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) - set_rule(world.get_location("Eastern Vault West Fuses"), - lambda state: state.has("Activate Beneath the Vault Fuse", player)) - set_rule(world.get_location("Eastern Vault East Fuse"), - lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", - "Activate South and West Fortress Exterior Fuses"}, player)) - set_rule(world.get_location("Quarry Connector Fuse"), - lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) - set_rule(world.get_location("Quarry Fuse"), - lambda state: state.has("Activate Quarry Connector Fuse", player)) - set_rule(world.get_location("Ziggurat Fuse"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("West Garden Fuse"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("Library Fuse"), - lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world)) - if not world.options.hexagon_quest: + if not bells_option: + set_rule(world.get_location("Eastern Bell"), + lambda state: (has_melee(state, player) or state.has(fire_wand, player))) + set_rule(world.get_location("Western Bell"), + lambda state: (has_melee(state, player) or state.has(fire_wand, player))) + if not fuses_option: + set_rule(world.get_location("Furnace Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("South and West Fortress Exterior Fuses"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Beneath the Vault Fuse"), + lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) + set_rule(world.get_location("Eastern Vault West Fuses"), + lambda state: state.has("Activate Beneath the Vault Fuse", player)) + set_rule(world.get_location("Eastern Vault East Fuse"), + lambda state: state.has_all(("Activate Upper and Central Fortress Exterior Fuses", + "Activate South and West Fortress Exterior Fuses"), player)) + set_rule(world.get_location("Quarry Connector Fuse"), + lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) + set_rule(world.get_location("Quarry Fuse"), + lambda state: state.has("Activate Quarry Connector Fuse", player)) + set_rule(world.get_location("Ziggurat Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("West Garden Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Library Fuse"), + lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world)) + if not options.hexagon_quest: set_rule(world.get_location("Place Questagons"), lambda state: state.has_all((red_hexagon, blue_hexagon, green_hexagon), player)) @@ -1868,7 +1865,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: # laurels past the enemies, then use the wand or gun to take care of the fairies that chased you add_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), - lambda state: state.has_any({fire_wand, "Gun"}, player)) + lambda state: state.has_any((fire_wand, gun), player)) combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden") combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden") combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden") @@ -1880,13 +1877,14 @@ def set_er_location_rules(world: "TunicWorld") -> None: # could just do the last two, but this outputs better in the spoiler log # dagger is maybe viable here, but it's sketchy -- activate ladder switch, save to reset enemies, climb up - combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress") - combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault") - combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress") + if not fuses_option: + combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress") + combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault") + combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress") # if you come in from the left, you only need to fight small crabs add_rule(world.get_location("Ruined Atoll - [South] Near Birds"), - lambda state: has_melee(state, player) or state.has_any({laurels, "Gun"}, player)) + lambda state: has_melee(state, player) or state.has_any((laurels, gun), player)) # can get this one without fighting if you have laurels add_rule(world.get_location("Frog's Domain - Above Vault"), @@ -1898,8 +1896,21 @@ def set_er_location_rules(world: "TunicWorld") -> None: and (state.has(laurels, player) or world.options.entrance_rando)) or has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), - lambda state: has_ability(prayer, state, world) - and has_combat_reqs("Rooted Ziggurat", state, player)) + lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) + + if fuses_option: + set_rule(world.get_location("Rooted Ziggurat Lower - [Miniboss] Activate Fuse"), + lambda state: has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)) + combat_logic_to_loc("Beneath the Fortress - Activate Fuse", "Beneath the Vault") + combat_logic_to_loc("Fortress Courtyard - [Upper] Activate Fuse", "Eastern Vault Fortress") + combat_logic_to_loc("Fortress Courtyard - [Central] Activate Fuse", "Eastern Vault Fortress") + combat_logic_to_loc("Eastern Vault Fortress - [Candle Room] Activate Fuse", "Eastern Vault Fortress") + combat_logic_to_loc("Eastern Vault Fortress - [Left of Door] Activate Fuse", "Eastern Vault Fortress") + combat_logic_to_loc("Eastern Vault Fortress - [Right of Door] Activate Fuse", "Eastern Vault Fortress") + combat_logic_to_loc("Ruined Atoll - [Northwest] Activate Fuse", "Ruined Atoll") + combat_logic_to_loc("Ruined Atoll - [Southwest] Activate Fuse", "Ruined Atoll") + combat_logic_to_loc("Swamp - [Central] Activate Fuse", "Swamp") # replace the sword rule with this one combat_logic_to_loc("Swamp - [South Graveyard] 4 Orange Skulls", "Swamp", set_instead=True) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 9fc44d84..81fb90d8 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,14 +1,16 @@ -from typing import Dict, List, Set, Tuple, TYPE_CHECKING +from copy import deepcopy +from random import Random +from typing import TYPE_CHECKING + from BaseClasses import Region, ItemClassification, Item, Location -from .locations import all_locations +from Options import PlandoConnection + +from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules from .er_data import (Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo, get_portal_outlet_region) from .er_rules import set_er_region_rules -from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules -from Options import PlandoConnection +from .locations import all_locations from .options import EntranceRando, EntranceLayout -from random import Random -from copy import deepcopy if TYPE_CHECKING: from . import TunicWorld @@ -22,8 +24,8 @@ class TunicERLocation(Location): game: str = "TUNIC" -def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: - regions: Dict[str, Region] = {} +def create_er_regions(world: "TunicWorld") -> dict[Portal, Portal]: + regions: dict[str, Region] = {} world.used_shop_numbers = set() for region_name, region_data in world.er_regions.items(): @@ -83,7 +85,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: # keys are event names, values are event regions -tunic_events: Dict[str, str] = { +tunic_events: dict[str, str] = { "Eastern Bell": "Forest Belltower Upper", "Western Bell": "Overworld Belltower at Bell", "Furnace Fuse": "Furnace Fuse", @@ -101,7 +103,7 @@ tunic_events: Dict[str, str] = { } -def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: +def place_event_items(world: "TunicWorld", regions: dict[str, Region]) -> None: for event_name, region_name in tunic_events.items(): region = regions[region_name] location = TunicERLocation(world.player, event_name, None, region) @@ -111,9 +113,13 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: location.place_locked_item( TunicERItem("Unseal the Heir", ItemClassification.progression, None, world.player)) elif event_name.endswith("Bell"): + # if world.options.shuffle_bells: + # continue location.place_locked_item( TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player)) - else: + elif event_name.endswith("Fuse") or event_name.endswith("Fuses"): + # if world.options.shuffle_fuses: + # continue location.place_locked_item( TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player)) region.locations.append(location) @@ -135,7 +141,7 @@ def get_shop_num(world: "TunicWorld") -> int: # all shops are the same shop. however, you cannot get to all shops from the same shop entrance. # so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back -def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_num) -> None: +def create_shop_region(world: "TunicWorld", regions: dict[str, Region], portal_num) -> None: new_shop_name = f"Shop {portal_num}" world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats) new_shop_region = Region(new_shop_name, world.player, world.multiworld) @@ -144,8 +150,8 @@ def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_n # for non-ER that uses the ER rules, we create a vanilla set of portal pairs -def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: - portal_pairs: Dict[Portal, Portal] = {} +def vanilla_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal, Portal]: + portal_pairs: dict[Portal, Portal] = {} # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here portal_map = [portal for portal in portal_mapping if portal.name not in ["Ziggurat Lower Falling Entrance", "Purgatory Bottom Exit", "Purgatory Top Exit"]] @@ -182,10 +188,10 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por # repeat this phase until all regions are reachable # second phase: randomly pair dead ends to random two_plus # third phase: randomly pair the remaining two_plus to each other -def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: - portal_pairs: Dict[Portal, Portal] = {} - dead_ends: List[Portal] = [] - two_plus: List[Portal] = [] +def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal, Portal]: + portal_pairs: dict[Portal, Portal] = {} + dead_ends: list[Portal] = [] + two_plus: list[Portal] = [] player_name = world.player_name portal_map = portal_mapping.copy() laurels_zips = world.options.laurels_zips.value @@ -194,6 +200,10 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal entrance_layout = world.options.entrance_layout laurels_location = world.options.laurels_location decoupled = world.options.decoupled + # shuffle_fuses = bool(world.options.shuffle_fuses.value) + # shuffle_bells = bool(world.options.shuffle_bells.value) + shuffle_fuses = False + shuffle_bells = False traversal_reqs = deepcopy(traversal_requirements) has_laurels = True waterfall_plando = False @@ -207,7 +217,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal entrance_layout = seed_group["entrance_layout"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False - logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) + logic_tricks: tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) # marking that you don't immediately have laurels if laurels_location == "10_fairies" and not world.using_ut: @@ -215,8 +225,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # for the direction pairs option with decoupled off # tracks how many portals are in each direction in each list - two_plus_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)} - dead_end_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)} + two_plus_direction_tracker: dict[int, int] = {direction: 0 for direction in range(8)} + dead_end_direction_tracker: dict[int, int] = {direction: 0 for direction in range(8)} # for ensuring we have enough entrances in directions left that we don't leave dead ends without any def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool: @@ -226,10 +236,6 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal return False return True - # If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit - if world.using_ut: - portal_map = portal_mapping.copy() - # create separate lists for dead ends and non-dead ends for portal in portal_map: dead_end_status = world.er_regions[portal.region].dead_end @@ -291,11 +297,12 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal dead_ends.append(shop_portal) dead_end_direction_tracker[shop_portal.direction] += 1 - connected_regions: Set[str] = set() + connected_regions: set[str] = set() # make better start region stuff when/if implementing random start start_region = "Overworld" connected_regions.add(start_region) - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks, + shuffle_fuses, shuffle_bells) if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value @@ -371,8 +378,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal else: modified_plando_connections = plando_connections - connected_shop_portal1s: Set[int] = set() - connected_shop_portal2s: Set[int] = set() + connected_shop_portal1s: set[int] = set() + connected_shop_portal2s: set[int] = set() for connection in modified_plando_connections: p_entrance = connection.entrance p_exit = connection.exit @@ -419,7 +426,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal break else: if p_entrance.startswith("Shop Portal "): - portal_num = int(p_entrance.split("Shop Portal ")[-1]) + try: + portal_num = int(p_entrance.split("Shop Portal ")[-1]) + except ValueError: + if "Previous Region" in p_entrance: + raise Exception("TUNIC: APWorld used for generation is incompatible with newer APWorld. " + "Please use the APWorld from Archipelago 0.6.1 instead.") + else: + raise Exception("TUNIC: Unknown error occurred in UT entrance setup, please contact " + "the TUNIC APWorld devs.") # shops 1-6 are south, 7 and 8 are east, and after that it just breaks direction pairs if portal_num <= 6: pdir = Direction.south @@ -452,7 +467,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal else: if not portal2: if p_exit.startswith("Shop Portal "): - portal_num = int(p_exit.split("Shop Portal ")[-1]) + try: + portal_num = int(p_exit.split("Shop Portal ")[-1]) + except ValueError: + if "Previous Region" in p_exit: + raise Exception("TUNIC: APWorld used for generation is incompatible with newer APWorld. " + "Please use the APWorld from Archipelago 0.6.1 instead.") + else: + raise Exception("TUNIC: Unknown error occurred in UT entrance setup, please contact " + "the TUNIC APWorld devs.") if portal_num <= 6: pdir = Direction.south elif portal_num in [7, 8]: @@ -510,13 +533,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal dead_end_direction_tracker[portal1.direction] -= 1 else: two_plus_direction_tracker[portal1.direction] -= 1 + if portal2_dead_end: dead_end_direction_tracker[portal2.direction] -= 1 else: two_plus_direction_tracker[portal2.direction] -= 1 # if we have plando connections, our connected regions may change somewhat - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks, + shuffle_fuses, shuffle_bells) # if there are an odd number of shops after plando, add another one, except in decoupled where it doesn't matter if not decoupled and len(world.used_shop_numbers) % 2 == 1: @@ -599,7 +624,6 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal connected_regions = backup_connected_regions.copy() rare_failure_count += 1 fail_count = 0 - if rare_failure_count > 100: raise Exception(f"Failed to pair regions due to rare pairing issues for {player_name}. " f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n" @@ -633,7 +657,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal if waterfall_plando: cr = connected_regions.copy() cr.add(portal.region) - if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks): + if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, + logic_tricks, shuffle_fuses, + shuffle_bells): continue # if not waterfall_plando, then we just want to pair secret gathering place now elif portal.region != "Secret Gathering Place": @@ -682,8 +708,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # once we have both portals, connect them and add the new region(s) to connected_regions if not has_laurels and "Secret Gathering Place" in connected_regions: has_laurels = True - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) - + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks, + shuffle_fuses, shuffle_bells) portal_pairs[portal1] = portal2 two_plus_direction_tracker[portal1.direction] -= 1 two_plus_direction_tracker[portal2.direction] -= 1 @@ -745,7 +771,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # loop through our list of paired portals and make two-way connections -def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None: +def create_randomized_entrances(world: "TunicWorld", portal_pairs: dict[Portal, Portal], regions: dict[str, Region]) -> None: for portal1, portal2 in portal_pairs.items(): # connect to the outlet region if there is one, if not connect to the actual region regions[portal1.region].connect( @@ -757,8 +783,9 @@ def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, name=portal2.name) -def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]], - has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]: +def update_reachable_regions(connected_regions: set[str], traversal_reqs: dict[str, dict[str, list[list[str]]]], + has_laurels: bool, logic: tuple[bool, int, int], shuffle_fuses: bool, + shuffle_bells: bool) -> set[str]: zips, ice_grapples, ls = logic # starting count, so we can run it again if this changes region_count = len(connected_regions) @@ -790,6 +817,12 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s break elif req not in connected_regions: break + elif req == "Fuse Shuffle": + if not shuffle_fuses: + break + elif req == "Bell Shuffle": + if not shuffle_bells: + break else: met_traversal_reqs = True break @@ -798,13 +831,14 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s # if the length of connected_regions changed, we got new regions, so we want to check those new origins if region_count != len(connected_regions): - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic, + shuffle_fuses, shuffle_bells) return connected_regions # which directions are opposites -direction_pairs: Dict[int, int] = { +direction_pairs: dict[int, int] = { Direction.north: Direction.south, Direction.south: Direction.north, Direction.east: Direction.west, @@ -848,9 +882,9 @@ def verify_plando_directions(connection: PlandoConnection) -> bool: # sort the portal dict by the name of the first portal, referring to the portal order in the master portal list -def sort_portals(portal_pairs: Dict[Portal, Portal], world: "TunicWorld") -> Dict[str, str]: - sorted_pairs: Dict[str, str] = {} - reference_list: List[str] = [portal.name for portal in portal_mapping] +def sort_portals(portal_pairs: dict[Portal, Portal], world: "TunicWorld") -> dict[str, str]: + sorted_pairs: dict[str, str] = {} + reference_list: list[str] = [portal.name for portal in portal_mapping] # due to plando, there can be a variable number of shops largest_shop_number = max(world.used_shop_numbers) diff --git a/worlds/tunic/fuses.py b/worlds/tunic/fuses.py new file mode 100644 index 00000000..4f223582 --- /dev/null +++ b/worlds/tunic/fuses.py @@ -0,0 +1,30 @@ +from .constants import * + +# for fuse locations and reusing event names to simplify er_rules +fuse_activation_reqs: dict[str, list[str]] = { + swamp_fuse_2: [swamp_fuse_1], + swamp_fuse_3: [swamp_fuse_1, swamp_fuse_2], + fortress_exterior_fuse_2: [fortress_exterior_fuse_1], + beneath_the_vault_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2], + fortress_candles_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], + fortress_door_left_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, + fortress_candles_fuse], + fortress_courtyard_upper_fuse: [fortress_exterior_fuse_1], + fortress_courtyard_lower_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse], + fortress_door_right_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, fortress_courtyard_lower_fuse], + quarry_fuse_2: [quarry_fuse_1], + "Activate Furnace Fuse": [west_furnace_fuse], + "Activate South and West Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2], + "Activate Upper and Central Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, + fortress_courtyard_lower_fuse], + "Activate Beneath the Vault Fuse": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], + "Activate Eastern Vault West Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, + fortress_candles_fuse, fortress_door_left_fuse], + "Activate Eastern Vault East Fuse": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, + fortress_courtyard_lower_fuse, fortress_door_right_fuse], + "Activate Quarry Connector Fuse": [quarry_fuse_1], + "Activate Quarry Fuse": [quarry_fuse_1, quarry_fuse_2], + "Activate Ziggurat Fuse": [ziggurat_teleporter_fuse], + "Activate West Garden Fuse": [west_garden_fuse], + "Activate Library Fuse": [library_lab_fuse], +} diff --git a/worlds/tunic/grass.py b/worlds/tunic/grass.py index 971ac4c0..f2aea40f 100644 --- a/worlds/tunic/grass.py +++ b/worlds/tunic/grass.py @@ -1,8 +1,11 @@ -from typing import Dict, NamedTuple, Optional, TYPE_CHECKING, Set +from typing import NamedTuple, TYPE_CHECKING from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule -from .rules import has_sword, has_melee + +from .constants import base_id +from .logic_helpers import has_sword, has_melee + if TYPE_CHECKING: from . import TunicWorld @@ -10,12 +13,12 @@ if TYPE_CHECKING: class TunicLocationData(NamedTuple): region: str er_region: str # entrance rando region - location_group: Optional[str] = None + location_group: str | None = None -location_base_id = 509342400 - -grass_location_table: Dict[str, TunicLocationData] = { +# todo: remove region, make all of these regions append grass to the name +# and then set the rules on the region entrances instead of the locations directly +grass_location_table: dict[str, TunicLocationData] = { "Overworld - Overworld Grass (576) (7.0, 4.0, -223.0)": TunicLocationData("Overworld", "Overworld"), "Overworld - Overworld Grass (572) (6.0, 4.0, -223.0)": TunicLocationData("Overworld", "Overworld"), "Overworld - Overworld Grass (574) (7.0, 4.0, -224.0)": TunicLocationData("Overworld", "Overworld"), @@ -7763,9 +7766,10 @@ excluded_grass_locations = { "Overworld - East Overworld Bush (64) (56.0, 44.0, -107.0)", } -grass_location_name_to_id: Dict[str, int] = {name: location_base_id + 302 + index for index, name in enumerate(grass_location_table)} +grass_base_id = base_id + 302 +grass_location_name_to_id: dict[str, int] = {name: grass_base_id + index for index, name in enumerate(grass_location_table)} -grass_location_name_groups: Dict[str, Set[str]] = {} +grass_location_name_groups: dict[str, set[str]] = {} for loc_name, loc_data in grass_location_table.items(): area_name = loc_name.split(" - ", 1)[0] # adding it to the normal location group and a grass-only one diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index a2b4140a..fe1e33e9 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -1,7 +1,10 @@ from itertools import groupby -from typing import Dict, List, Set, NamedTuple, Optional +from typing import NamedTuple + from BaseClasses import ItemClassification as IC +from .constants import base_id + class TunicItemData(NamedTuple): classification: IC @@ -9,12 +12,10 @@ class TunicItemData(NamedTuple): item_id_offset: int item_group: str = "" # classification if combat logic is on - combat_ic: Optional[IC] = None + combat_ic: None | IC = None -item_base_id = 509342400 - -item_table: Dict[str, TunicItemData] = { +item_table: dict[str, TunicItemData] = { "Firecracker x2": TunicItemData(IC.filler, 3, 0, "Bombs"), "Firecracker x3": TunicItemData(IC.filler, 3, 1, "Bombs"), "Firecracker x4": TunicItemData(IC.filler, 3, 2, "Bombs"), @@ -175,7 +176,7 @@ item_table: Dict[str, TunicItemData] = { } # items to be replaced by fool traps -fool_tiers: List[List[str]] = [ +fool_tiers: list[list[str]] = [ [], ["Money x1", "Money x10", "Money x15", "Money x16"], ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"], @@ -214,25 +215,25 @@ slot_data_item_names = [ "Gold Questagon", ] -combat_items: List[str] = [name for name, data in item_table.items() +combat_items: list[str] = [name for name, data in item_table.items() if data.combat_ic and IC.progression in data.combat_ic] combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels", "Gun"]) -item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} +item_name_to_id: dict[str, int] = {name: base_id + data.item_id_offset for name, data in item_table.items()} -filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler and name != "Grass"] +filler_items: list[str] = [name for name, data in item_table.items() if data.classification == IC.filler and name != "Grass"] def get_item_group(item_name: str) -> str: return item_table[item_name].item_group -item_name_groups: Dict[str, Set[str]] = { +item_name_groups: dict[str, set[str]] = { group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != "" } # extra groups for the purpose of aliasing items -extra_groups: Dict[str, Set[str]] = { +extra_groups: dict[str, set[str]] = { "Laurels": {"Hero's Laurels"}, "Orb": {"Magic Orb"}, "Dagger": {"Magic Dagger"}, diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py index f2d4b944..99a51b40 100644 --- a/worlds/tunic/ladder_storage_data.py +++ b/worlds/tunic/ladder_storage_data.py @@ -1,15 +1,15 @@ -from typing import Dict, List, Set, NamedTuple, Optional +from typing import NamedTuple # ladders in overworld, since it is the most complex area for ladder storage class OWLadderInfo(NamedTuple): - ladders: Set[str] # ladders where the top or bottom is at the same elevation - portals: List[str] # portals at the same elevation, only those without doors - regions: List[str] # regions where a melee enemy can hit you out of ladder storage + ladders: set[str] # ladders where the top or bottom is at the same elevation + portals: list[str] # portals at the same elevation, only those without doors + regions: list[str] # regions where a melee enemy can hit you out of ladder storage # groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets -ow_ladder_groups: Dict[str, OWLadderInfo] = { +ow_ladder_groups: dict[str, OWLadderInfo] = { # lowest elevation "LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"}, ["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_", @@ -49,7 +49,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = { # ladders accessible within different regions of overworld, only those that are relevant # other scenes will just have them hardcoded since this type of structure is not necessary there -region_ladders: Dict[str, Set[str]] = { +region_ladders: dict[str, set[str]] = { "Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"}, "Overworld Beach": {"Ladder to Ruined Atoll"}, @@ -63,11 +63,11 @@ region_ladders: Dict[str, Set[str]] = { class LadderInfo(NamedTuple): origin: str # origin region destination: str # destination portal - ladders_req: Optional[str] = None # ladders required to do this + ladders_req: str | None = None # ladders required to do this dest_is_region: bool = False # whether it is a region that you are going to -easy_ls: List[LadderInfo] = [ +easy_ls: list[LadderInfo] = [ # In the furnace # Furnace ladder to the fuse entrance LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"), @@ -128,7 +128,7 @@ easy_ls: List[LadderInfo] = [ ] # if we can gain elevation or get knocked down, add the harder ones -medium_ls: List[LadderInfo] = [ +medium_ls: list[LadderInfo] = [ # region-destination versions of easy ls spots LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True), # fortress courtyard knockdowns are never logically relevant, the fuse requires upper @@ -169,7 +169,7 @@ medium_ls: List[LadderInfo] = [ LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"), ] -hard_ls: List[LadderInfo] = [ +hard_ls: list[LadderInfo] = [ # lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"), LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"), diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index ced3d223..93c6164b 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -1,17 +1,19 @@ -from typing import Dict, NamedTuple, Set, Optional, List -from .grass import grass_location_table +from typing import NamedTuple + +# from .bells import bell_location_table from .breakables import breakable_location_table +from .constants import base_id +# from .fuses import fuse_location_table +from .grass import grass_location_table class TunicLocationData(NamedTuple): region: str er_region: str # entrance rando region - location_group: Optional[str] = None + location_group: str | None = None -location_base_id = 509342400 - -location_table: Dict[str, TunicLocationData] = { +location_table: dict[str, TunicLocationData] = { "Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"), "Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), "Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), @@ -243,7 +245,7 @@ location_table: Dict[str, TunicLocationData] = { "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"), "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"), "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"), - "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"), + "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Miniboss Platform"), "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 Mid Checkpoint"), "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), @@ -307,7 +309,7 @@ location_table: Dict[str, TunicLocationData] = { "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden after Terry"), "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden South Checkpoint"), "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden South Checkpoint"), - "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden before Boss"), + "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden South Checkpoint"), "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 South Checkpoint"), "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), @@ -316,19 +318,21 @@ location_table: Dict[str, TunicLocationData] = { "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), } -hexagon_locations: Dict[str, str] = { +hexagon_locations: dict[str, str] = { "Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup", "Green Questagon": "Librarian - Hexagon Green", "Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue", } -standard_location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)} +standard_location_name_to_id: dict[str, int] = {name: base_id + index for index, name in enumerate(location_table)} all_locations = location_table.copy() all_locations.update(grass_location_table) all_locations.update(breakable_location_table) +# all_locations.update(fuse_location_table) +# all_locations.update(bell_location_table) -location_name_groups: Dict[str, Set[str]] = {} +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) diff --git a/worlds/tunic/logic_helpers.py b/worlds/tunic/logic_helpers.py new file mode 100644 index 00000000..1752bf8e --- /dev/null +++ b/worlds/tunic/logic_helpers.py @@ -0,0 +1,98 @@ +from typing import TYPE_CHECKING + +from BaseClasses import CollectionState + +from .constants import * +from .fuses import fuse_activation_reqs +from .options import HexagonQuestAbilityUnlockType, IceGrappling + +if TYPE_CHECKING: + from . import TunicWorld + + +def randomize_ability_unlocks(world: "TunicWorld") -> dict[str, int]: + options = world.options + + abilities = [prayer, holy_cross, icebolt] + ability_requirement = [1, 1, 1] + world.random.shuffle(abilities) + + if options.hexagon_quest.value and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: + hexagon_goal = options.hexagon_goal.value + # Set ability unlocks to 25, 50, and 75% of goal amount + ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4] + if any(req == 0 for req in ability_requirement): + ability_requirement = [1, 2, 3] + + return dict(zip(abilities, ability_requirement)) + + +def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool: + options = world.options + ability_unlocks = world.ability_unlocks + if not options.ability_shuffling: + return True + if options.hexagon_quest and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: + return state.has(gold_hexagon, world.player, ability_unlocks[ability]) + return state.has(ability, world.player) + + +# a check to see if you can whack things in melee at all +def has_melee(state: CollectionState, player: int) -> bool: + return state.has_any(("Stick", "Sword", "Sword Upgrade"), player) + + +def has_sword(state: CollectionState, player: int) -> bool: + return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) + + +def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.laurels_zips and state.has(laurels, world.player) + + +def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool: + if world.options.ice_grappling < difficulty: + return False + if not long_range: + return state.has_all((ice_dagger, grapple), world.player) + else: + return state.has_all((ice_dagger, fire_wand, grapple), world.player) and has_ability(icebolt, state, world) + + +def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: + if not world.options.ladder_storage: + return False + if world.options.ladder_storage_without_items: + return True + return has_melee(state, world.player) or state.has_any((grapple, shield), world.player) + + +def has_mask(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.maskless or state.has(mask, world.player) + + +def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.lanternless or state.has(lantern, world.player) + + +def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: + return not world.options.shuffle_ladders or state.has(ladder, world.player) + + +def can_shop(state: CollectionState, world: "TunicWorld") -> bool: + return has_sword(state, world.player) and state.can_reach_region("Shop", world.player) + + +# for the ones that are not early bushes where ER can screw you over a bit +def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool: + # add in glass cannon + stick for grass rando + return has_sword(state, world.player) or state.has_any((fire_wand, laurels, gun), world.player) + + +def has_fuses(fuse_event: str, state: CollectionState, world: "TunicWorld") -> bool: + player = world.player + fuses_option = False # replace fuses_option with world.options.shuffle_fuses when fuse shuffle is in + if fuses_option: + return state.has_all(fuse_activation_reqs[fuse_event], player) + + return state.has(fuse_event, player) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 09e2d1d6..79bb033b 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,12 +1,13 @@ -import logging from dataclasses import dataclass -from typing import Dict, Any, TYPE_CHECKING - from decimal import Decimal, ROUND_HALF_UP +import logging +from typing import Any, TYPE_CHECKING from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, PerGameCommonOptions, OptionGroup, Removed, Visibility, NamedRange) + from .er_data import portal_mapping + if TYPE_CHECKING: from . import TunicWorld @@ -145,17 +146,6 @@ class EntranceRando(TextChoice): default = 0 -class FixedShop(Toggle): - """ - This option has been superseded by the Entrance Layout option. - If enabled, it will override the Entrance Layout option. - This is kept to keep older yamls working, and will be removed at a later date. - """ - visibility = Visibility.none - internal_name = "fixed_shop" - display_name = "Fewer Shops in Entrance Rando" - - class EntranceLayout(Choice): """ Decide how the Entrance Randomizer chooses how to pair the entrances. @@ -219,8 +209,8 @@ class GrassRandomizer(Toggle): class LocalFill(NamedRange): """ Choose the percentage of your filler/trap items that will be kept local or distributed to other TUNIC players with this option enabled. + This option defaults to 95% if you have Grass Randomizer enabled, 40% if you have Breakable Shuffle enabled, 96% if you have both, and 0% otherwise. If you have Grass Randomizer enabled, this option must be set to 95% or higher to avoid flooding the item pool. The host can remove this restriction by turning off the limit_grass_rando setting in host.yaml. - This option defaults to 95% if you have Grass Randomizer enabled, and to 0% otherwise. This option ignores items placed in your local_items or non_local_items. This option does nothing in single player games. """ @@ -332,6 +322,14 @@ class LadderStorageWithoutItems(Toggle): display_name = "Ladder Storage without Items" +class BreakableShuffle(Toggle): + """ + Turns approximately 250 breakable objects in the game into checks. + """ + internal_name = "breakable_shuffle" + display_name = "Breakable Shuffle" + + class HiddenAllRandom(Toggle): """ Sets all options that can be random to random. @@ -342,36 +340,9 @@ class HiddenAllRandom(Toggle): visibility = Visibility.none -class LogicRules(Choice): - """ - This option has been superseded by the individual trick options. - If set to nmg, it will set Ice Grappling to medium and Laurels Zips on. - If set to ur, it will do nmg as well as set Ladder Storage to medium. - It is here to avoid breaking old yamls, and will be removed at a later date. - """ - visibility = Visibility.none - internal_name = "logic_rules" - display_name = "Logic Rules" - option_restricted = 0 - option_no_major_glitches = 1 - alias_nmg = 1 - option_unrestricted = 2 - alias_ur = 2 - default = 0 - - -class BreakableShuffle(Toggle): - """ - Turns approximately 250 breakable objects in the game into checks. - """ - internal_name = "breakable_shuffle" - display_name = "Breakable Shuffle" - - @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool - sword_progression: SwordProgression start_with_sword: StartWithSword keys_behind_bosses: KeysBehindBosses @@ -386,6 +357,8 @@ class TunicOptions(PerGameCommonOptions): hexagon_quest_ability_type: HexagonQuestAbilityUnlockType shuffle_ladders: ShuffleLadders + # shuffle_fuses: ShuffleFuses + # shuffle_bells: ShuffleBells grass_randomizer: GrassRandomizer breakable_shuffle: BreakableShuffle local_fill: LocalFill @@ -393,7 +366,6 @@ class TunicOptions(PerGameCommonOptions): entrance_rando: EntranceRando entrance_layout: EntranceLayout decoupled: Decoupled - plando_connections: TunicPlandoConnections combat_logic: CombatLogic lanternless: Lanternless @@ -402,11 +374,13 @@ class TunicOptions(PerGameCommonOptions): ice_grappling: IceGrappling ladder_storage: LadderStorage ladder_storage_without_items: LadderStorageWithoutItems - + + plando_connections: TunicPlandoConnections + all_random: HiddenAllRandom - fixed_shop: FixedShop # will be removed at a later date - logic_rules: Removed # fully removed in the direction pairs update + fixed_shop: Removed + logic_rules: Removed tunic_option_groups = [ @@ -433,7 +407,7 @@ tunic_option_groups = [ ]), ] -tunic_option_presets: Dict[str, Dict[str, Any]] = { +tunic_option_presets: dict[str, dict[str, Any]] = { "Sync": { "ability_shuffling": True, }, @@ -460,14 +434,16 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = { def check_options(world: "TunicWorld"): options = world.options - if options.hexagon_quest and options.ability_shuffling and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: + if (options.hexagon_quest and options.ability_shuffling + and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons): total_hexes = get_hexagons_in_pool(world) min_hexes = 3 if options.keys_behind_bosses: min_hexes = 15 if total_hexes < min_hexes: - logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.") + logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability " + "Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.") options.hexagon_quest_ability_type.value = HexagonQuestAbilityUnlockType.option_pages @@ -475,4 +451,4 @@ def get_hexagons_in_pool(world: "TunicWorld"): # Calculate number of hexagons in item pool options = world.options return min(int((Decimal(100 + options.extra_hexagon_percentage) / 100 * options.hexagon_goal) - .to_integral_value(rounding=ROUND_HALF_UP)), 100) + .to_integral_value(rounding=ROUND_HALF_UP)), 100) diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py deleted file mode 100644 index f21af11e..00000000 --- a/worlds/tunic/regions.py +++ /dev/null @@ -1,25 +0,0 @@ -tunic_regions: dict[str, tuple[str]] = { - "Menu": ("Overworld",), - "Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", - "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", - "Spirit Arena"), - "Overworld Holy Cross": tuple(), - "East Forest": tuple(), - "Dark Tomb": ("West Garden",), - "Beneath the Well": tuple(), - "West Garden": tuple(), - "Ruined Atoll": ("Frog's Domain", "Library"), - "Frog's Domain": tuple(), - "Library": tuple(), - "Eastern Vault Fortress": ("Beneath the Vault",), - "Beneath the Vault": ("Eastern Vault Fortress",), - "Quarry Back": ("Quarry", "Monastery"), - "Quarry": ("Monastery", "Lower Quarry"), - "Monastery": ("Monastery Back",), - "Monastery Back": tuple(), - "Lower Quarry": ("Rooted Ziggurat",), - "Rooted Ziggurat": tuple(), - "Swamp": ("Cathedral",), - "Cathedral": tuple(), - "Spirit Arena": tuple() -} diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py deleted file mode 100644 index 52d5c42e..00000000 --- a/worlds/tunic/rules.py +++ /dev/null @@ -1,402 +0,0 @@ -from typing import Dict, TYPE_CHECKING - -from worlds.generic.Rules import set_rule, forbid_item, add_rule -from BaseClasses import CollectionState -from .options import LadderStorage, IceGrappling, HexagonQuestAbilityUnlockType -if TYPE_CHECKING: - from . import TunicWorld - -laurels = "Hero's Laurels" -grapple = "Magic Orb" -ice_dagger = "Magic Dagger" -fire_wand = "Magic Wand" -gun = "Gun" -lantern = "Lantern" -fairies = "Fairy" -coins = "Golden Coin" -prayer = "Pages 24-25 (Prayer)" -holy_cross = "Pages 42-43 (Holy Cross)" -icebolt = "Pages 52-53 (Icebolt)" -shield = "Shield" -key = "Key" -house_key = "Old House Key" -vault_key = "Fortress Vault Key" -mask = "Scavenger Mask" -red_hexagon = "Red Questagon" -green_hexagon = "Green Questagon" -blue_hexagon = "Blue Questagon" -gold_hexagon = "Gold Questagon" - -# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules -bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", - "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", - "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"] - - -def randomize_ability_unlocks(world: "TunicWorld") -> Dict[str, int]: - random = world.random - options = world.options - - abilities = [prayer, holy_cross, icebolt] - ability_requirement = [1, 1, 1] - random.shuffle(abilities) - - if options.hexagon_quest.value and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: - hexagon_goal = options.hexagon_goal.value - # Set ability unlocks to 25, 50, and 75% of goal amount - ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4] - if any(req == 0 for req in ability_requirement): - ability_requirement = [1, 2, 3] - - return dict(zip(abilities, ability_requirement)) - - -def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool: - options = world.options - ability_unlocks = world.ability_unlocks - if not options.ability_shuffling: - return True - if options.hexagon_quest and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons: - return state.has(gold_hexagon, world.player, ability_unlocks[ability]) - return state.has(ability, world.player) - - -# a check to see if you can whack things in melee at all -def has_melee(state: CollectionState, player: int) -> bool: - return state.has_any({"Stick", "Sword", "Sword Upgrade"}, player) - - -def has_sword(state: CollectionState, player: int) -> bool: - return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) - - -def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.laurels_zips and state.has(laurels, world.player) - - -def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool: - if world.options.ice_grappling < difficulty: - return False - if not long_range: - return state.has_all({ice_dagger, grapple}, world.player) - else: - return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) - - -def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: - if not world.options.ladder_storage: - return False - if world.options.ladder_storage_without_items: - return True - return has_melee(state, world.player) or state.has_any((grapple, shield), world.player) - - -def has_mask(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.maskless or state.has(mask, world.player) - - -def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.lanternless or state.has(lantern, world.player) - - -def set_region_rules(world: "TunicWorld") -> None: - player = world.player - options = world.options - - world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \ - lambda state: has_ability(holy_cross, state, world) - world.get_entrance("Overworld -> Beneath the Well").access_rule = \ - lambda state: has_melee(state, player) or state.has(fire_wand, player) - world.get_entrance("Overworld -> Dark Tomb").access_rule = \ - lambda state: has_lantern(state, world) - # laurels in, ladder storage in through the furnace, or ice grapple down the belltower - world.get_entrance("Overworld -> West Garden").access_rule = \ - lambda state: (state.has(laurels, player) - or can_ladder_storage(state, world) - or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ - lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \ - or can_ladder_storage(state, world) - # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules - world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ - lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) - # there's some boxes in the way - and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand), player))) - world.get_entrance("Ruined Atoll -> Library").access_rule = \ - lambda state: (state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) - and (has_sword(state, player) or state.has_any((fire_wand, gun), player))) - world.get_entrance("Overworld -> Quarry").access_rule = \ - lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ - and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world)) - world.get_entrance("Quarry Back -> Quarry").access_rule = \ - lambda state: has_sword(state, player) or state.has(fire_wand, player) - world.get_entrance("Quarry Back -> Monastery").access_rule = \ - lambda state: state.has(laurels, player) - world.get_entrance("Monastery -> Monastery Back").access_rule = \ - lambda state: (has_sword(state, player) or state.has(fire_wand, player) - or laurels_zip(state, world)) - world.get_entrance("Quarry -> Lower Quarry").access_rule = \ - lambda state: has_mask(state, world) - world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ - lambda state: state.has(grapple, player) and has_ability(prayer, state, world) - world.get_entrance("Swamp -> Cathedral").access_rule = \ - lambda state: (state.has(laurels, player) and has_ability(prayer, state, world) and has_sword(state, player)) \ - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - world.get_entrance("Overworld -> Spirit Arena").access_rule = \ - lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value - else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) - and state.has_group_unique("Hero Relics", player, 6)) - and has_ability(prayer, state, world) and has_sword(state, player) - and state.has_any({lantern, laurels}, player)) - - world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"), - rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world) - and has_ability(prayer, state, world)) - - if options.ladder_storage >= LadderStorage.option_medium: - # ls at any ladder in a safe spot in quarry to get to the monastery rope entrance - add_rule(world.get_entrance(entrance_name="Quarry Back -> Monastery"), - rule=lambda state: can_ladder_storage(state, world)) - - -def set_location_rules(world: "TunicWorld") -> None: - player = world.player - - forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) - - # Ability Shuffle Exclusive Rules - set_rule(world.get_location("Far Shore - Page Pickup"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("Fortress Courtyard - Chest Near Cave"), - lambda state: has_ability(prayer, state, world) - or state.has(laurels, player) - or can_ladder_storage(state, world) - or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) - and has_lantern(state, world))) - set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), - lambda state: has_ability(prayer, state, world) or state.has(laurels, player) - or can_ladder_storage(state, world) - or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) - and has_lantern(state, world))) - set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), - lambda state: has_ability(prayer, state, world)) - set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Library Hall - Holy Cross Chest"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), - lambda state: has_ability(holy_cross, state, world)) - set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), - lambda state: has_ability(holy_cross, state, world)) - - # Overworld - set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), - lambda state: state.has_any({grapple, laurels}, player)) - set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), - lambda state: state.has_any({grapple, laurels}, player)) - set_rule(world.get_location("Far Shore - Secret Chest"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Old House - Normal Chest"), - lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or laurels_zip(state, world)) - set_rule(world.get_location("Old House - Holy Cross Chest"), - lambda state: has_ability(holy_cross, state, world) and ( - state.has(house_key, player) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or laurels_zip(state, world))) - set_rule(world.get_location("Old House - Shield Pickup"), - lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or laurels_zip(state, world)) - set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Overworld - [Southwest] From West Garden"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Overworld - [West] Chest After Bell"), - lambda state: state.has(laurels, player) - or (has_lantern(state, world) and has_sword(state, player)) - or can_ladder_storage(state, world)) - set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), - lambda state: state.has_any({grapple, laurels}, player)) - set_rule(world.get_location("Overworld - [East] Grapple Chest"), - lambda state: state.has(grapple, player)) - set_rule(world.get_location("Special Shop - Secret Page Pickup"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), - lambda state: has_ability(holy_cross, state, world) - and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) - or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) - set_rule(world.get_location("Sealed Temple - Page Pickup"), - lambda state: state.has(laurels, player) - or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - set_rule(world.get_location("West Furnace - Lantern Pickup"), - lambda state: has_melee(state, player) or state.has_any({fire_wand, laurels}, player)) - - set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), - lambda state: state.has(fairies, player, 10)) - set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), - lambda state: state.has(fairies, player, 20)) - set_rule(world.get_location("Coins in the Well - 3 Coins"), - lambda state: state.has(coins, player, 3)) - set_rule(world.get_location("Coins in the Well - 6 Coins"), - lambda state: state.has(coins, player, 6)) - set_rule(world.get_location("Coins in the Well - 10 Coins"), - lambda state: state.has(coins, player, 10)) - set_rule(world.get_location("Coins in the Well - 15 Coins"), - lambda state: state.has(coins, player, 15)) - - # East Forest - set_rule(world.get_location("East Forest - Lower Grapple Chest"), - lambda state: state.has(grapple, player)) - set_rule(world.get_location("East Forest - Lower Dash Chest"), - lambda state: state.has_all({grapple, laurels}, player)) - set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), - lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) - and has_ability(icebolt, state, world)) - - # West Garden - set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), - lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) - set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), - lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) - or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), - lambda state: state.has(laurels, player) - or (has_lantern(state, world) and has_sword(state, player)) - or can_ladder_storage(state, world)) - - # Ruined Atoll - set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), - lambda state: state.has(laurels, player)) - # ice grapple push a crab through the door - set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: has_sword(state, player)) - - # Frog's Domain - set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), - lambda state: state.has_any({grapple, laurels}, player)) - set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), - lambda state: state.has_any({grapple, laurels}, player)) - set_rule(world.get_location("Frog's Domain - Escape Chest"), - lambda state: state.has_any({grapple, laurels}, player)) - - # Library Lab - set_rule(world.get_location("Library Lab - Page 1"), - lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) - set_rule(world.get_location("Library Lab - Page 2"), - lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) - set_rule(world.get_location("Library Lab - Page 3"), - lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) - - # Eastern Vault Fortress - # yes, you can clear the leaves with dagger - # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have - # but really, I expect the player to just throw a bomb at them if they don't have melee - set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), - lambda state: state.has(laurels, player) and (has_melee(state, player) or state.has(ice_dagger, player))) - set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), - lambda state: has_sword(state, player) - and (has_ability(prayer, state, world) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) - set_rule(world.get_location("Fortress Arena - Hexagon Red"), - lambda state: state.has(vault_key, player) - and (has_ability(prayer, state, world) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) - - # Beneath the Vault - set_rule(world.get_location("Beneath the Fortress - Bridge"), - lambda state: has_lantern(state, world) and - (has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player))) - set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"), - lambda state: has_melee(state, player) and has_lantern(state, world)) - - # Quarry - set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), - lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player)) - set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player)) - - # Swamp - set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), - lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) - set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial"), - lambda state: state.has(laurels, player)) - set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), - lambda state: has_sword(state, player)) - - # Hero's Grave - set_rule(world.get_location("Hero's Grave - Tooth Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Hero's Grave - Mushroom Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Hero's Grave - Ash Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Hero's Grave - Flowers Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Hero's Grave - Effigy Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(world.get_location("Hero's Grave - Feathers Relic"), - lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - - # Bombable Walls - for location_name in bomb_walls: - # has_sword is there because you can buy bombs in the shop - set_rule(world.get_location(location_name), - lambda state: state.has(gun, player) - or has_sword(state, player) - or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - add_rule(world.get_location("Cube Cave - Holy Cross Chest"), - lambda state: state.has(gun, player) - or has_sword(state, player) - or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # can't ice grapple to this one, not enough space - set_rule(world.get_location("Quarry - [East] Bombable Wall"), - lambda state: state.has(gun, player) or has_sword(state, player)) - - # Shop - set_rule(world.get_location("Shop - Potion 1"), - lambda state: has_sword(state, player)) - set_rule(world.get_location("Shop - Potion 2"), - lambda state: has_sword(state, player)) - set_rule(world.get_location("Shop - Coin 1"), - lambda state: has_sword(state, player)) - set_rule(world.get_location("Shop - Coin 2"), - lambda state: has_sword(state, player)) diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 1896db5d..f5d429ac 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -5,13 +5,6 @@ from .bases import TunicTestBase class TestAccess(TunicTestBase): options = {options.CombatLogic.internal_name: options.CombatLogic.option_off} - # test whether you can get into the temple without laurels - def test_temple_access(self) -> None: - self.collect_all_but(["Hero's Laurels", "Lantern"]) - self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup")) - self.collect_by_name(["Lantern"]) - self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup")) - # test that the wells function properly. Since fairies is written the same way, that should succeed too def test_wells(self) -> None: self.collect_all_but(["Golden Coin"]) @@ -50,22 +43,12 @@ class TestHexQuestNoShuffle(TunicTestBase): self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) -class TestNormalGoal(TunicTestBase): - options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false} - - # test that you need the three colored hexes to reach the Heir in standard - def test_normal_goal(self) -> None: - location = ["The Heir"] - items = [["Red Questagon", "Blue Questagon", "Green Questagon"]] - self.assertAccessDependency(location, items) - - class TestER(TunicTestBase): options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, options.HexagonQuest.internal_name: options.HexagonQuest.option_false, options.CombatLogic.internal_name: options.CombatLogic.option_off, - options.FixedShop.internal_name: options.FixedShop.option_true} + options.EntranceLayout.internal_name: options.EntranceLayout.option_fixed_shop} def test_overworld_hc_chest(self) -> None: # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld @@ -99,7 +82,7 @@ class TestLadderStorage(TunicTestBase): options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, options.HexagonQuest.internal_name: options.HexagonQuest.option_false, - options.FixedShop.internal_name: options.FixedShop.option_false, + options.EntranceLayout.internal_name: options.EntranceLayout.option_standard, options.LadderStorage.internal_name: options.LadderStorage.option_hard, options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false, "plando_connections": [