diff --git a/worlds/astalon/bases.py b/worlds/astalon/bases.py index f50d1a1618c0..1a5c9383548f 100644 --- a/worlds/astalon/bases.py +++ b/worlds/astalon/bases.py @@ -1,27 +1,14 @@ -from abc import ABCMeta -from collections import defaultdict -from typing import TYPE_CHECKING, Any, ClassVar +from typing import ClassVar from BaseClasses import MultiWorld -from worlds.AutoWorld import AutoWorldRegister, World +from worlds.AutoWorld import World from .items import Character, EarlyItems from .options import AstalonOptions from .settings import AstalonSettings -if TYPE_CHECKING: - from .logic.instances import RuleInstance - -# TODO: remove once ap 0.6.6 is released -class AstalonWorldMetaclass(AutoWorldRegister, ABCMeta): - def __new__(mcs, name: str, bases: tuple[type, ...], dct: dict[str, Any]) -> AutoWorldRegister: - if name == "AstalonWorld": - return super().__new__(mcs, name, bases, dct) - return super(AutoWorldRegister, mcs).__new__(mcs, name, bases, dct) - - -class AstalonWorldBase(World, metaclass=AstalonWorldMetaclass): +class AstalonWorldBase(World): options_dataclass = AstalonOptions settings: ClassVar[AstalonSettings] # pyright: ignore[reportIncompatibleVariableOverride] @@ -31,12 +18,7 @@ class AstalonWorldBase(World, metaclass=AstalonWorldMetaclass): early_items: EarlyItems portal_pairs: tuple[tuple[str, str], ...] = () - rule_cache: "dict[int, RuleInstance]" - _rule_deps: dict[str, set[int]] - def __init__(self, multiworld: MultiWorld, player: int) -> None: super().__init__(multiworld, player) self.starting_characters = [] self.early_items = EarlyItems() - self.rule_cache = {} - self._rule_deps = defaultdict(set) diff --git a/worlds/astalon/constants.py b/worlds/astalon/constants.py index 759b2a7a3037..36dbb976f2be 100644 --- a/worlds/astalon/constants.py +++ b/worlds/astalon/constants.py @@ -1,4 +1,3 @@ from typing import Final GAME_NAME: Final[str] = "Astalon" -BASE_ID: Final[int] = 333000 diff --git a/worlds/astalon/items.py b/worlds/astalon/items.py index 94e9372b512a..2914844b17af 100644 --- a/worlds/astalon/items.py +++ b/worlds/astalon/items.py @@ -3,15 +3,12 @@ from enum import StrEnum from functools import cached_property from itertools import groupby -from typing import TYPE_CHECKING, TypeAlias +from typing import TypeAlias from BaseClasses import Item, ItemClassification -from .constants import BASE_ID, GAME_NAME -from .options import StartingLocation - -if TYPE_CHECKING: - from . import AstalonWorld +from .constants import GAME_NAME +from .options import AstalonOptions, StartingLocation class ItemGroup(StrEnum): @@ -529,7 +526,7 @@ class AstalonItem(Item): @dataclass(frozen=True) class ItemData: name: ItemName - classification: ItemClassification | Callable[["AstalonWorld"], ItemClassification] + classification: ItemClassification | Callable[[AstalonOptions], ItemClassification] quantity_in_item_pool: int group: ItemGroup description: str = "" @@ -615,11 +612,9 @@ class ItemData: ItemData(BlueDoor.CAVES, ItemClassification.progression, 1, ItemGroup.DOOR_BLUE), ItemData( BlueDoor.CATA_ORBS, - lambda world: ( - ItemClassification.progression - if world.options.randomize_candles or world.options.randomize_orb_multipliers - else ItemClassification.useful - ), + lambda options: ItemClassification.progression + if options.randomize_candles or options.randomize_orb_multipliers + else ItemClassification.useful, 1, ItemGroup.DOOR_BLUE, ), @@ -630,9 +625,7 @@ class ItemData: ItemData(BlueDoor.CATA_PRISON_LEFT, ItemClassification.filler, 1, ItemGroup.DOOR_BLUE), ItemData( BlueDoor.CATA_PRISON_RIGHT, - lambda world: ( - ItemClassification.progression if world.options.randomize_candles else ItemClassification.filler - ), + lambda options: ItemClassification.progression if options.randomize_candles else ItemClassification.filler, 1, ItemGroup.DOOR_BLUE, ), @@ -691,21 +684,17 @@ class ItemData: ItemData(Switch.GT_UPPER_PATH_ACCESS, ItemClassification.progression, 1, ItemGroup.SWITCH), ItemData( Switch.GT_CROSSES, - lambda world: ( - ItemClassification.filler - if world.options.open_early_doors and world.options.starting_location == StartingLocation.option_gorgon_tomb - else ItemClassification.progression - ), + lambda options: ItemClassification.filler + if options.open_early_doors and options.starting_location == StartingLocation.option_gorgon_tomb + else ItemClassification.progression, 1, ItemGroup.SWITCH, ), ItemData( Switch.GT_GH_SHORTCUT, - lambda world: ( - ItemClassification.filler - if world.options.open_early_doors and world.options.starting_location == StartingLocation.option_gorgon_tomb - else ItemClassification.progression - ), + lambda options: ItemClassification.filler + if options.open_early_doors and options.starting_location == StartingLocation.option_gorgon_tomb + else ItemClassification.progression, 1, ItemGroup.SWITCH, ), @@ -904,8 +893,8 @@ class ItemData: ItemData(Orbs.ORB_MULTI, ItemClassification.useful, 3, ItemGroup.ORBS), ) -item_table = {item.name.value: item for item in ALL_ITEMS} -item_name_to_id: dict[str, int] = {data.name.value: i for i, data in enumerate(ALL_ITEMS, start=BASE_ID)} +item_table: dict[str, ItemData] = {item.name.value: item for item in ALL_ITEMS} +item_name_to_id: dict[str, int] = {data.name.value: i for i, data in enumerate(ALL_ITEMS, start=1)} def get_item_group(item_name: str) -> ItemGroup: diff --git a/worlds/astalon/locations.py b/worlds/astalon/locations.py index f50737eea701..8ffd3d49615a 100644 --- a/worlds/astalon/locations.py +++ b/worlds/astalon/locations.py @@ -4,7 +4,7 @@ from BaseClasses import Location -from .constants import BASE_ID, GAME_NAME +from .constants import GAME_NAME from .regions import RegionName @@ -517,7 +517,7 @@ class LocationName(StrEnum): class AstalonLocation(Location): - game: str = GAME_NAME + game = GAME_NAME @dataclass(frozen=True) @@ -1116,8 +1116,8 @@ class LocationData: LocationData(LocationName.CATA_ORB_MULTI, RegionName.CATA_MULTI, LocationGroup.ORBS, Area.CATA), ) -location_table = {location.name.value: location for location in ALL_LOCATIONS} -location_name_to_id: dict[str, int] = {data.name.value: i for i, data in enumerate(ALL_LOCATIONS, start=BASE_ID)} +location_table: dict[str, LocationData] = {location.name.value: location for location in ALL_LOCATIONS} +location_name_to_id: dict[str, int] = {data.name.value: i for i, data in enumerate(ALL_LOCATIONS, start=1)} def get_location_group(location_name: str) -> LocationGroup: diff --git a/worlds/astalon/logic/__init__.py b/worlds/astalon/logic/__init__.py index d2df41e26498..e69de29bb2d1 100644 --- a/worlds/astalon/logic/__init__.py +++ b/worlds/astalon/logic/__init__.py @@ -1,10 +0,0 @@ -from .instances import RuleInstance -from .main_campaign import MAIN_ENTRANCE_RULES, MAIN_LOCATION_RULES -from .mixin import AstalonLogicMixin - -__all__ = ( - "MAIN_ENTRANCE_RULES", - "MAIN_LOCATION_RULES", - "AstalonLogicMixin", - "RuleInstance", -) diff --git a/worlds/astalon/logic/custom_rules.py b/worlds/astalon/logic/custom_rules.py new file mode 100644 index 000000000000..f48951b5dd3a --- /dev/null +++ b/worlds/astalon/logic/custom_rules.py @@ -0,0 +1,285 @@ +import dataclasses +from collections.abc import Iterable +from enum import Enum +from typing import ClassVar, cast + +from typing_extensions import override + +from BaseClasses import CollectionState +from NetUtils import JSONMessagePart +from Options import Option +from rule_builder import rules +from rule_builder.options import OptionFilter + +from ..bases import AstalonWorldBase +from ..constants import GAME_NAME +from ..items import ( + BlueDoor, + Crystal, + Elevator, + Events, + Eye, + Face, + ItemName, + KeyItem, + RedDoor, + Switch, + WhiteDoor, +) +from ..locations import LocationName +from ..options import ( + Difficulty, + Goal, + RandomizeBlueKeys, + RandomizeElevator, + RandomizeRedKeys, + RandomizeSwitches, + RandomizeWhiteKeys, +) +from ..regions import RegionName + + +def as_str(value: Enum | str | None) -> str: + if value is None: + return "" + return value.value if isinstance(value, Enum) else value + + +@dataclasses.dataclass(init=False) +class Has(rules.Has[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + item_name: ItemName | Events, + count: int = 1, + *, + options: Iterable[OptionFilter] = (), + filtered_resolution: bool = False, + ) -> None: + super().__init__(as_str(item_name), count, options=options, filtered_resolution=filtered_resolution) + + +@dataclasses.dataclass(init=False) +class HasAll(rules.HasAll[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + *item_names: ItemName | Events, + options: Iterable[OptionFilter] = (), + ) -> None: + names = [as_str(name) for name in item_names] + if len(names) != len(set(names)): + raise ValueError(f"Duplicate items detected, likely typo, items: {names}") + + super().__init__(*names, options=options) + + +@dataclasses.dataclass(init=False) +class HasAny(rules.HasAny[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + *item_names: ItemName | Events, + options: Iterable[OptionFilter] = (), + ) -> None: + names = [as_str(name) for name in item_names] + if len(names) != len(set(names)): + raise ValueError(f"Duplicate items detected, likely typo, items: {names}") + + super().__init__(*names, options=options) + + +@dataclasses.dataclass(init=False) +class CanReachLocation(rules.CanReachLocation[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + location_name: LocationName, + parent_region_name: RegionName | None = None, + skip_indirect_connection: bool = False, + *, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(as_str(location_name), as_str(parent_region_name), skip_indirect_connection, options=options) + + +@dataclasses.dataclass(init=False) +class CanReachRegion(rules.CanReachRegion[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + region_name: RegionName, + *, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(as_str(region_name), options=options) + + +@dataclasses.dataclass(init=False) +class CanReachEntrance(rules.CanReachEntrance[AstalonWorldBase], game=GAME_NAME): + @override + def __init__( + self, + from_region: RegionName, + to_region: RegionName, + *, + options: Iterable[OptionFilter] = (), + ) -> None: + entrance_name = f"{as_str(from_region)} -> {as_str(to_region)}" + super().__init__(entrance_name, as_str(from_region), options=options) + + +@dataclasses.dataclass(init=False) +class ToggleRule(HasAll, game=GAME_NAME): + option_cls: ClassVar[type[Option[int]]] + otherwise: bool = False + + @override + def _instantiate(self, world: AstalonWorldBase) -> rules.Rule.Resolved: + items = tuple(cast(ItemName | Events, item) for item in self.item_names) + if len(items) == 1: + rule = Has(items[0], options=[OptionFilter(self.option_cls, 1)]) + else: + rule = HasAll(*items, options=[OptionFilter(self.option_cls, 1)]) + + if self.otherwise: + return rules.Or( + rule, + rules.True_(options=[OptionFilter(self.option_cls, 0)]), + ).resolve(world) + + return rule.resolve(world) + + +@dataclasses.dataclass(init=False) +class HasWhite(ToggleRule, game=GAME_NAME): + option_cls: ClassVar[type[Option[int]]] = RandomizeWhiteKeys + + def __init__( + self, + *doors: WhiteDoor, + otherwise: bool = False, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(*doors, options=options) + self.otherwise: bool = otherwise + + +@dataclasses.dataclass(init=False) +class HasBlue(ToggleRule, game=GAME_NAME): + option_cls: ClassVar[type[Option[int]]] = RandomizeBlueKeys + + def __init__( + self, + *doors: BlueDoor, + otherwise: bool = False, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(*doors, options=options) + self.otherwise: bool = otherwise + + +@dataclasses.dataclass(init=False) +class HasRed(ToggleRule, game=GAME_NAME): + option_cls: ClassVar[type[Option[int]]] = RandomizeRedKeys + + def __init__( + self, + *doors: RedDoor, + otherwise: bool = False, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(*doors, options=options) + self.otherwise: bool = otherwise + + +@dataclasses.dataclass(init=False) +class HasSwitch(ToggleRule, game=GAME_NAME): + option_cls: ClassVar[type[Option[int]]] = RandomizeSwitches + + def __init__( + self, + *switches: Switch | Crystal | Face, + otherwise: bool = False, + options: Iterable[OptionFilter] = (), + ) -> None: + super().__init__(*switches, options=options) + self.otherwise: bool = otherwise + + +@dataclasses.dataclass(init=False) +class HasElevator(HasAll, game=GAME_NAME): + def __init__(self, elevator: Elevator, *, options: Iterable[OptionFilter] = ()) -> None: + super().__init__( + KeyItem.ASCENDANT_KEY, + elevator, + options=[*options, OptionFilter(RandomizeElevator, RandomizeElevator.option_true)], + ) + + +@dataclasses.dataclass() +class HasGoal(rules.Rule[AstalonWorldBase], game=GAME_NAME): + @override + def _instantiate(self, world: AstalonWorldBase) -> rules.Rule.Resolved: + if world.options.goal.value != Goal.option_eye_hunt: + return rules.True_().resolve(world) + return Has(Eye.GOLD, world.options.additional_eyes_required.value).resolve(world) + + +@dataclasses.dataclass() +class HardLogic(rules.WrapperRule[AstalonWorldBase], game=GAME_NAME): + @override + def _instantiate(self, world: AstalonWorldBase) -> rules.Rule.Resolved: + if world.options.difficulty.value == Difficulty.option_hard: + return self.child.resolve(world) + if getattr(world.multiworld, "generation_is_fake", False): + return self.Resolved( + self.child.resolve(world), + player=world.player, + caching_enabled=getattr(world, "rule_caching_enabled", False), + ) + return rules.False_().resolve(world) + + class Resolved(rules.WrapperRule.Resolved): + @override + def _evaluate(self, state: CollectionState) -> bool: + return state.has(Events.FAKE_OOL_ITEM.value, self.player) and self.child(state) + + @override + def item_dependencies(self) -> dict[str, set[int]]: + deps = super().item_dependencies() + deps.setdefault(Events.FAKE_OOL_ITEM.value, set()).add(id(self)) + return deps + + @override + def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]: + messages: list[JSONMessagePart] = [ + {"type": "color", "color": "glitched", "text": "Hard Logic ["}, + ] + messages.extend(self.child.explain_json(state)) + messages.append({"type": "color", "color": "glitched", "text": "]"}) + return messages + + +@dataclasses.dataclass() +class CampfireWarp(rules.True_[AstalonWorldBase], game=GAME_NAME): + name: str + + @override + def _instantiate(self, world: AstalonWorldBase) -> "Resolved": + return self.Resolved( + self.name, + player=world.player, + caching_enabled=getattr(world, "rule_caching_enabled", False), + ) + + class Resolved(rules.True_.Resolved): + name: str + + @override + def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]: + return [{"type": "color", "color": "green", "text": f"Campfire Warp to {self.name}"}] + + @override + def __str__(self) -> str: + return f"Campfire Warp to {self.name}" diff --git a/worlds/astalon/logic/factories.py b/worlds/astalon/logic/factories.py deleted file mode 100644 index 04df1981a7cc..000000000000 --- a/worlds/astalon/logic/factories.py +++ /dev/null @@ -1,476 +0,0 @@ -import dataclasses -import operator -from typing import TYPE_CHECKING, Any, ClassVar - -from ..items import Character, Events, Eye, KeyItem, ShopUpgrade -from ..options import Difficulty, Goal, RandomizeCharacters -from .instances import ( - AndInstance, - CanReachEntranceInstance, - CanReachLocationInstance, - CanReachRegionInstance, - FalseInstance, - HardLogicInstance, - HasAllInstance, - HasAnyInstance, - HasInstance, - NestedRuleInstance, - OrInstance, - TrueInstance, -) - -if TYPE_CHECKING: - from Options import CommonOptions, Option - - from ..items import BlueDoor, Crystal, Elevator, Face, ItemName, RedDoor, Switch, WhiteDoor - from ..locations import LocationName - from ..regions import RegionName - from ..world import AstalonWorld - from .instances import RuleInstance - - -ITEM_DEPS: "dict[str, tuple[Character, ...]]" = { - KeyItem.CLOAK.value: (Character.ALGUS,), - KeyItem.SWORD.value: (Character.ARIAS,), - KeyItem.BOOTS.value: (Character.ARIAS,), - KeyItem.CLAW.value: (Character.KYULI,), - KeyItem.BOW.value: (Character.KYULI,), - KeyItem.BLOCK.value: (Character.ZEEK,), - KeyItem.STAR.value: (Character.BRAM,), - KeyItem.BANISH.value: (Character.ALGUS, Character.ZEEK), - KeyItem.GAUNTLET.value: (Character.ARIAS, Character.BRAM), - ShopUpgrade.ALGUS_ARCANIST.value: (Character.ALGUS,), - ShopUpgrade.ALGUS_METEOR.value: (Character.ALGUS,), - ShopUpgrade.ALGUS_SHOCK.value: (Character.ALGUS,), - ShopUpgrade.ARIAS_GORGONSLAYER.value: (Character.ARIAS,), - ShopUpgrade.ARIAS_LAST_STAND.value: (Character.ARIAS,), - ShopUpgrade.ARIAS_LIONHEART.value: (Character.ARIAS,), - ShopUpgrade.KYULI_ASSASSIN.value: (Character.KYULI,), - ShopUpgrade.KYULI_BULLSEYE.value: (Character.KYULI,), - ShopUpgrade.KYULI_RAY.value: (Character.KYULI,), - ShopUpgrade.ZEEK_JUNKYARD.value: (Character.ZEEK,), - ShopUpgrade.ZEEK_ORBS.value: (Character.ZEEK,), - ShopUpgrade.ZEEK_LOOT.value: (Character.ZEEK,), - ShopUpgrade.BRAM_AXE.value: (Character.BRAM,), - ShopUpgrade.BRAM_HUNTER.value: (Character.BRAM,), - ShopUpgrade.BRAM_WHIPLASH.value: (Character.BRAM,), -} - -VANILLA_CHARACTERS = frozenset((Character.ALGUS, Character.ARIAS, Character.KYULI)) -OPERATORS = { - "eq": operator.eq, - "ne": operator.ne, - "gt": operator.gt, - "lt": operator.lt, - "ge": operator.ge, - "le": operator.le, - "contains": operator.contains, -} - -characters_off = ("randomize_characters", 0) -characters_on = ("randomize_characters__ge", 1) - - -@dataclasses.dataclass() -class RuleFactory: - options: dict[str, Any] = dataclasses.field(default_factory=dict, kw_only=True) - - instance_cls: "ClassVar[type[RuleInstance]]" - - def _passes_options(self, options: "CommonOptions") -> bool: - for key, value in self.options.items(): - parts = key.split("__", maxsplit=1) - option_name = parts[0] - operator = parts[1] if len(parts) > 1 else "eq" - opt: Option[Any] = getattr(options, option_name) - if not OPERATORS[operator](opt.value, value): - return False - return True - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - return self.instance_cls(player=world.player) - - def resolve(self, world: "AstalonWorld") -> "RuleInstance": - if not self._passes_options(world.options): - return FalseInstance(player=world.player) - - instance = self._instantiate(world) - rule_hash = hash(instance) - if rule_hash not in world.rule_cache: - world.rule_cache[rule_hash] = instance - return world.rule_cache[rule_hash] - - def serialize(self) -> str: - return f"{self.__class__.__name__}()" - - -@dataclasses.dataclass() -class True_(RuleFactory): - instance_cls = TrueInstance - - def serialize(self) -> str: - return "True" - - -@dataclasses.dataclass() -class False_(RuleFactory): - instance_cls = FalseInstance - - def serialize(self) -> str: - return "False" - - -@dataclasses.dataclass(init=False) -class NestedRuleFactory(RuleFactory): - children: "tuple[RuleFactory, ...]" - - instance_cls = NestedRuleInstance - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - children = [c.resolve(world) for c in self.children] - return self.instance_cls(tuple(children), player=world.player).simplify() # type: ignore - - def __init__(self, *children: "RuleFactory", options: dict[str, Any] | None = None) -> None: - super().__init__(options=options or {}) - self.children = children - - -@dataclasses.dataclass(init=False) -class And(NestedRuleFactory): - instance_cls = AndInstance - - def serialize(self) -> str: - return f"({' + '.join(child.serialize() for child in self.children)})" - - -@dataclasses.dataclass(init=False) -class Or(NestedRuleFactory): - instance_cls = OrInstance - - def serialize(self) -> str: - return f"({' | '.join(child.serialize() for child in self.children)})" - - -@dataclasses.dataclass() -class Has(RuleFactory): - item: "ItemName | Events" - count: int = 1 - - instance_cls = HasInstance - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - default = HasInstance(self.item.value, self.count, player=world.player) - - if self.item in VANILLA_CHARACTERS: - if world.options.randomize_characters == RandomizeCharacters.option_vanilla: - return TrueInstance(player=world.player) - return default - - if deps := ITEM_DEPS.get(self.item): - if world.options.randomize_characters == RandomizeCharacters.option_vanilla and ( - len(deps) > 1 or (len(deps) == 1 and deps[0] in VANILLA_CHARACTERS) - ): - return default - if len(deps) == 1: - return HasAllInstance((deps[0].value, self.item.value), player=world.player) - return OrInstance( - tuple(HasAllInstance((d.value, self.item.value), player=world.player) for d in deps), - player=world.player, - ) - - return default - - def serialize(self) -> str: - return f"Has({self.item.value})" - - -@dataclasses.dataclass(init=False) -class HasAll(RuleFactory): - items: "tuple[ItemName | Events, ...]" - - instance_cls = HasAllInstance - - def __init__(self, *items: "ItemName | Events", options: dict[str, Any] | None = None) -> None: - if len(items) != len(set(items)): - raise ValueError(f"Duplicate items detected, likely typo, items: {items}") - - super().__init__(options=options or {}) - self.items = items - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - if len(self.items) == 0: - return TrueInstance(player=world.player) - if len(self.items) == 1: - return Has(self.items[0]).resolve(world) - - new_clauses: list[RuleInstance] = [] - new_items: list[str] = [] - for item in self.items: - if item in VANILLA_CHARACTERS and world.options.randomize_characters == RandomizeCharacters.option_vanilla: - continue - deps = ITEM_DEPS.get(item, []) - if not deps: - new_items.append(item.value) - continue - - if len(deps) > 1: - if world.options.randomize_characters == RandomizeCharacters.option_vanilla: - new_items.append(item.value) - else: - new_clauses.append( - OrInstance( - tuple(HasAllInstance((d.value, item.value), player=world.player) for d in deps), - player=world.player, - ) - ) - continue - - if ( - len(deps) == 1 - and deps[0] not in self.items - and not ( - deps[0] in VANILLA_CHARACTERS - and world.options.randomize_characters == RandomizeCharacters.option_vanilla - ) - ): - new_items.append(deps[0].value) - - new_items.append(item.value) - - if len(new_clauses) == 0 and len(new_items) == 0: - return TrueInstance(player=world.player) - if len(new_items) == 1: - new_clauses.append(HasInstance(new_items[0], player=world.player)) - elif len(new_items) > 1: - new_clauses.append(HasAllInstance(tuple(new_items), player=world.player)) - if len(new_clauses) == 0: - return FalseInstance(player=world.player) - if len(new_clauses) == 1: - return new_clauses[0] - return AndInstance(tuple(new_clauses), player=world.player) - - def serialize(self) -> str: - return f"HasAll({', '.join(i.value for i in self.items)})" - - -@dataclasses.dataclass(init=False) -class HasAny(RuleFactory): - items: "tuple[ItemName | Events, ...]" - - instance_cls = HasAnyInstance - - def __init__(self, *items: "ItemName | Events", options: dict[str, Any] | None = None) -> None: - if len(items) != len(set(items)): - raise ValueError(f"Duplicate items detected, likely typo, items: {items}") - - super().__init__(options=options or {}) - self.items = items - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - if len(self.items) == 0: - return TrueInstance(player=world.player) - if len(self.items) == 1: - return Has(self.items[0]).resolve(world) - - new_clauses: list[RuleInstance] = [] - new_items: list[str] = [] - for item in self.items: - if item in VANILLA_CHARACTERS and world.options.randomize_characters == RandomizeCharacters.option_vanilla: - return TrueInstance(player=world.player) - - deps = ITEM_DEPS.get(item, []) - if not deps: - new_items.append(item.value) - continue - - if len(deps) > 1: - if world.options.randomize_characters == RandomizeCharacters.option_vanilla: - new_items.append(item.value) - else: - new_clauses.append( - OrInstance( - tuple(HasAllInstance((d.value, item.value), player=world.player) for d in deps), - player=world.player, - ) - ) - continue - - if ( - len(deps) == 1 - and deps[0] not in self.items - and not ( - deps[0] in VANILLA_CHARACTERS - and world.options.randomize_characters == RandomizeCharacters.option_vanilla - ) - ): - new_clauses.append(HasAllInstance((deps[0].value, item.value), player=world.player)) - else: - new_items.append(item.value) - - if len(new_items) == 1: - new_clauses.append(HasInstance(new_items[0], player=world.player)) - elif len(new_items) > 1: - new_clauses.append(HasAnyInstance(tuple(new_items), player=world.player)) - - if len(new_clauses) == 0: - return FalseInstance(player=world.player) - if len(new_clauses) == 1: - return new_clauses[0] - return OrInstance(tuple(new_clauses), player=world.player) - - def serialize(self) -> str: - return f"HasAny({', '.join(i.value for i in self.items)})" - - -@dataclasses.dataclass() -class CanReachLocation(RuleFactory): - location: "LocationName" - - instance_cls = CanReachLocationInstance - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - location = world.get_location(self.location.value) - if not location.parent_region: - raise ValueError(f"Location {location.name} has no parent region") - return CanReachLocationInstance(location.name, location.parent_region.name, player=world.player) - - def serialize(self) -> str: - return f"CanReachLocation({self.location.value})" - - -@dataclasses.dataclass() -class CanReachRegion(RuleFactory): - region: "RegionName" - - instance_cls = CanReachRegionInstance - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - return CanReachRegionInstance(self.region.value, player=world.player) - - def serialize(self) -> str: - return f"CanReachRegion({self.region.value})" - - -@dataclasses.dataclass() -class CanReachEntrance(RuleFactory): - from_region: "RegionName" - to_region: "RegionName" - - instance_cls = CanReachEntranceInstance - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - entrance = f"{self.from_region.value} -> {self.to_region.value}" - return CanReachEntranceInstance(entrance, player=world.player) - - def serialize(self) -> str: - return f"CanReachEntrance({self.from_region.value} -> {self.to_region.value})" - - -@dataclasses.dataclass(init=False) -class ToggleRule(HasAll): - option_name: ClassVar[str] - otherwise: bool = False - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - if len(self.items) == 1: - rule = Has(self.items[0], options={self.option_name: 1}) - else: - rule = HasAll(*self.items, options={self.option_name: 1}) - - if self.otherwise: - return Or( - rule, - True_(options={self.option_name: 0}), - ).resolve(world) - - return rule.resolve(world) - - -@dataclasses.dataclass(init=False) -class HasWhite(ToggleRule): - option_name = "randomize_white_keys" - - def __init__( - self, - *doors: "WhiteDoor", - otherwise: bool = False, - options: dict[str, Any] | None = None, - ) -> None: - super().__init__(*doors, options=options) - self.otherwise = otherwise - - -@dataclasses.dataclass(init=False) -class HasBlue(ToggleRule): - option_name = "randomize_blue_keys" - - def __init__( - self, - *doors: "BlueDoor", - otherwise: bool = False, - options: dict[str, Any] | None = None, - ) -> None: - super().__init__(*doors, options=options) - self.otherwise = otherwise - - -@dataclasses.dataclass(init=False) -class HasRed(ToggleRule): - option_name = "randomize_red_keys" - - def __init__( - self, - *doors: "RedDoor", - otherwise: bool = False, - options: dict[str, Any] | None = None, - ) -> None: - super().__init__(*doors, options=options) - self.otherwise = otherwise - - -@dataclasses.dataclass(init=False) -class HasSwitch(ToggleRule): - option_name = "randomize_switches" - - def __init__( - self, - *switches: "Switch | Crystal | Face", - otherwise: bool = False, - options: dict[str, Any] | None = None, - ) -> None: - super().__init__(*switches, options=options) - self.otherwise = otherwise - - -@dataclasses.dataclass(init=False) -class HasElevator(HasAll): - def __init__(self, elevator: "Elevator", *, options: dict[str, Any] | None = None) -> None: - options = options or {} - super().__init__(KeyItem.ASCENDANT_KEY, elevator, options={**options, "randomize_elevator": 1}) - - -@dataclasses.dataclass() -class HasGoal(RuleFactory): - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - if world.options.goal != Goal.option_eye_hunt: - return TrueInstance(player=world.player) - return HasInstance( - Eye.GOLD.value, - count=world.options.additional_eyes_required.value, - player=world.player, - ) - - -@dataclasses.dataclass() -class HardLogic(RuleFactory): - child: "RuleFactory" - - def _instantiate(self, world: "AstalonWorld") -> "RuleInstance": - if world.options.difficulty.value == Difficulty.option_hard: - return self.child.resolve(world) - if getattr(world.multiworld, "generation_is_fake", False): - return HardLogicInstance(self.child.resolve(world), player=world.player) - return FalseInstance(player=world.player) - - def serialize(self) -> str: - return f"HardLogic[{self.child.serialize()}]" diff --git a/worlds/astalon/logic/instances.py b/worlds/astalon/logic/instances.py deleted file mode 100644 index 65c289fd3ddd..000000000000 --- a/worlds/astalon/logic/instances.py +++ /dev/null @@ -1,445 +0,0 @@ -import dataclasses -import itertools -from typing import TYPE_CHECKING, ClassVar - -from ..items import Events -from ..regions import RegionName - -if TYPE_CHECKING: - from BaseClasses import CollectionState - from NetUtils import JSONMessagePart - - -def _printjson_item(item: str, player: int, state: "CollectionState | None" = None) -> "JSONMessagePart": - message: JSONMessagePart = {"type": "item_name", "flags": 0b001, "text": item, "player": player} - if state: - color = "green" if state.has(item, player) else "salmon" - if item == Events.FAKE_OOL_ITEM: - color = "glitched" - message["color"] = color - return message - - -@dataclasses.dataclass(kw_only=True, frozen=True) -class RuleInstance: - player: int - cacheable: bool = dataclasses.field(repr=False, default=True) - - always_true: ClassVar = False - always_false: ClassVar = False - - def __hash__(self) -> int: - return hash((self.__class__.__name__, *[getattr(self, f.name) for f in dataclasses.fields(self)])) - - def _evaluate(self, state: "CollectionState") -> bool: ... - - def evaluate(self, state: "CollectionState") -> bool: - result = self._evaluate(state) - if self.cacheable: - state._astalon_rule_results[self.player][id(self)] = result # type: ignore - return result - - def test(self, state: "CollectionState") -> bool: - cached_result = None - if self.cacheable: - cached_result = state._astalon_rule_results[self.player].get(id(self)) # type: ignore - if cached_result is not None: - return cached_result - return self.evaluate(state) - - def deps(self) -> "dict[str, set[int]]": - return {} - - def indirect(self) -> "tuple[RegionName, ...]": - return () - - def serialize(self) -> str: - return f"{self.__class__.__name__}()" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [{"type": "text", "text": self.__class__.__name__}] - - -@dataclasses.dataclass(frozen=True) -class TrueInstance(RuleInstance): - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - always_true: ClassVar = True - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return True - - def serialize(self) -> str: - return "True" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [{"type": "color", "color": "green", "text": "True"}] - - -@dataclasses.dataclass(frozen=True) -class FalseInstance(RuleInstance): - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - always_false: ClassVar = True - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return False - - def serialize(self) -> str: - return "False" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [{"type": "color", "color": "salmon", "text": "False"}] - - -@dataclasses.dataclass(frozen=True) -class NestedRuleInstance(RuleInstance): - children: "tuple[RuleInstance, ...]" - - def deps(self) -> "dict[str, set[int]]": - combined_deps: dict[str, set[int]] = {} - for child in self.children: - for item_name, rules in child.deps().items(): - if item_name in combined_deps: - combined_deps[item_name] |= rules - else: - combined_deps[item_name] = {id(self), *rules} - return combined_deps - - def indirect(self) -> "tuple[RegionName, ...]": - return tuple(itertools.chain.from_iterable(child.indirect() for child in self.children)) - - def simplify(self) -> "RuleInstance": - return self - - -@dataclasses.dataclass(frozen=True) -class AndInstance(NestedRuleInstance): - def _evaluate(self, state: "CollectionState") -> bool: - for rule in self.children: - if not rule.test(state): - return False - return True - - def serialize(self) -> str: - return f"({' + '.join(child.serialize() for child in self.children)})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [{"type": "text", "text": "("}] - for i, child in enumerate(self.children): - if i > 0: - messages.append({"type": "text", "text": " & "}) - messages.extend(child.explain(state)) - messages.append({"type": "text", "text": ")"}) - return messages - - def simplify(self) -> "RuleInstance": - children_to_process = list(self.children) - clauses: list[RuleInstance] = [] - items: set[str] = set() - true_rule: RuleInstance | None = None - - while children_to_process: - child = children_to_process.pop(0) - if child.always_false: - # false always wins - return child - if child.always_true: - # dedupe trues - true_rule = child - continue - if isinstance(child, AndInstance): - children_to_process.extend(child.children) - continue - - if isinstance(child, HasInstance) and child.count == 1: - items.add(child.item) - elif isinstance(child, HasAllInstance): - items.update(child.items) - else: - clauses.append(child) - - if not clauses and not items: - return true_rule or FalseInstance(player=self.player) - if items: - if len(items) == 1: - item_rule = HasInstance(items.pop(), player=self.player) - else: - item_rule = HasAllInstance(tuple(sorted(items)), player=self.player) - if not clauses: - return item_rule - clauses.append(item_rule) - - if len(clauses) == 1: - return clauses[0] - return AndInstance( - tuple(clauses), - player=self.player, - cacheable=self.cacheable and all(c.cacheable for c in clauses), - ) - - -@dataclasses.dataclass(frozen=True) -class OrInstance(NestedRuleInstance): - def _evaluate(self, state: "CollectionState") -> bool: - for rule in self.children: - if rule.test(state): - return True - return False - - def serialize(self) -> str: - return f"({' | '.join(child.serialize() for child in self.children)})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [{"type": "text", "text": "("}] - for i, child in enumerate(self.children): - if i > 0: - messages.append({"type": "text", "text": " | "}) - messages.extend(child.explain(state)) - messages.append({"type": "text", "text": ")"}) - return messages - - def simplify(self) -> "RuleInstance": - children_to_process = list(self.children) - clauses: list[RuleInstance] = [] - items: set[str] = set() - - while children_to_process: - child = children_to_process.pop(0) - if child.always_true: - # true always wins - return child - if child.always_false: - # falses can be ignored - continue - if isinstance(child, OrInstance): - children_to_process.extend(child.children) - continue - - if isinstance(child, HasInstance) and child.count == 1: - items.add(child.item) - elif isinstance(child, HasAnyInstance): - items.update(child.items) - else: - clauses.append(child) - - if not clauses and not items: - return FalseInstance(player=self.player) - if items: - if len(items) == 1: - item_rule = HasInstance(items.pop(), player=self.player) - else: - item_rule = HasAnyInstance(tuple(sorted(items)), player=self.player) - if not clauses: - return item_rule - clauses.append(item_rule) - - if len(clauses) == 1: - return clauses[0] - return OrInstance( - tuple(clauses), - player=self.player, - cacheable=self.cacheable and all(c.cacheable for c in clauses), - ) - - -@dataclasses.dataclass(frozen=True) -class HasInstance(RuleInstance): - item: str - count: int = 1 - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return state.has(self.item, self.player, count=self.count) - - def deps(self) -> dict[str, set[int]]: - return {self.item: {id(self)}} - - def serialize(self) -> str: - count_display = f", count={self.count}" if self.count > 1 else "" - return f"Has({self.item}{count_display})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [{"type": "text", "text": "Has "}] - if self.count > 1: - messages.append({"type": "color", "color": "cyan", "text": str(self.count)}) - messages.append({"type": "text", "text": "x "}) - messages.append(_printjson_item(self.item, self.player, state)) - return messages - - -@dataclasses.dataclass(frozen=True) -class HasAllInstance(RuleInstance): - items: tuple[str, ...] - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return state.has_all(self.items, self.player) - - def deps(self) -> dict[str, set[int]]: - return {item: {id(self)} for item in self.items} - - def serialize(self) -> str: - return f"HasAll({', '.join(self.items)})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [ - {"type": "text", "text": "Has "}, - {"type": "color", "color": "cyan", "text": "all"}, - {"type": "text", "text": " of ("}, - ] - for i, item in enumerate(self.items): - if i > 0: - messages.append({"type": "text", "text": ", "}) - messages.append(_printjson_item(item, self.player, state)) - messages.append({"type": "text", "text": ")"}) - return messages - - -@dataclasses.dataclass(frozen=True) -class HasAnyInstance(RuleInstance): - items: tuple[str, ...] - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return state.has_any(self.items, self.player) - - def deps(self) -> dict[str, set[int]]: - return {item: {id(self)} for item in self.items} - - def serialize(self) -> str: - return f"HasAny({', '.join(self.items)})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [ - {"type": "text", "text": "Has "}, - {"type": "color", "color": "cyan", "text": "any"}, - {"type": "text", "text": " of ("}, - ] - for i, item in enumerate(self.items): - if i > 0: - messages.append({"type": "text", "text": ", "}) - messages.append(_printjson_item(item, self.player, state)) - messages.append({"type": "text", "text": ")"}) - return messages - - -@dataclasses.dataclass(frozen=True) -class CanReachLocationInstance(RuleInstance): - location: str - parent_region: str - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - def _evaluate(self, state: "CollectionState") -> bool: - return state.can_reach_location(self.location, self.player) - - def indirect(self) -> "tuple[RegionName, ...]": - return (RegionName(self.parent_region),) - - def serialize(self) -> str: - return f"CanReachLocation({self.location})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [ - {"type": "text", "text": "Reached Location "}, - {"type": "location_name", "text": self.location, "player": self.player}, - ] - - -@dataclasses.dataclass(frozen=True) -class CanReachRegionInstance(RuleInstance): - region: str - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - def _evaluate(self, state: "CollectionState") -> bool: - return state.can_reach_region(self.region, self.player) - - def indirect(self) -> "tuple[RegionName, ...]": - return (RegionName(self.region),) - - def serialize(self) -> str: - return f"CanReachRegion({self.region})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [ - {"type": "text", "text": "Reached Region "}, - {"type": "color", "color": "yellow", "text": self.region}, - ] - - -@dataclasses.dataclass(frozen=True) -class CanReachEntranceInstance(RuleInstance): - entrance: str - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - def _evaluate(self, state: "CollectionState") -> bool: - return state.can_reach_entrance(self.entrance, self.player) - - def serialize(self) -> str: - return f"CanReachEntrance({self.entrance})" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [ - {"type": "text", "text": "Reached Entrance "}, - {"type": "entrance_name", "text": self.entrance, "player": self.player}, - ] - - -@dataclasses.dataclass(frozen=True) -class HardLogicInstance(RuleInstance): - child: RuleInstance - - def _evaluate(self, state: "CollectionState") -> bool: - return state.has(Events.FAKE_OOL_ITEM.value, self.player) and self.child.test(state) - - def deps(self) -> "dict[str, set[int]]": - deps = self.child.deps() - deps.setdefault(Events.FAKE_OOL_ITEM.value, set()).add(id(self)) - return deps - - def indirect(self) -> "tuple[RegionName, ...]": - return self.child.indirect() - - def serialize(self) -> str: - return f"HardLogic[{self.child.serialize()}]" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - messages: list[JSONMessagePart] = [ - {"type": "color", "color": "glitched", "text": "Hard Logic ["}, - ] - messages.extend(self.child.explain(state)) - messages.append({"type": "color", "color": "glitched", "text": "]"}) - return messages - - -@dataclasses.dataclass(frozen=True) -class CampfireWarpInstance(RuleInstance): - name: str - - cacheable: bool = dataclasses.field(repr=False, default=False, init=False) - - always_true: ClassVar = True - - def __hash__(self) -> int: - return super().__hash__() - - def _evaluate(self, state: "CollectionState") -> bool: - return True - - def serialize(self) -> str: - return f"Campfire Warp to {self.name}" - - def explain(self, state: "CollectionState | None" = None) -> "list[JSONMessagePart]": - return [{"type": "color", "color": "green", "text": f"Campfire Warp to {self.name}"}] diff --git a/worlds/astalon/logic/main_campaign.py b/worlds/astalon/logic/main_campaign.py index 005ec42f2d20..7581ae0f76ab 100644 --- a/worlds/astalon/logic/main_campaign.py +++ b/worlds/astalon/logic/main_campaign.py @@ -1,8 +1,13 @@ +from rule_builder.options import OptionFilter +from rule_builder.rules import Filtered, Rule, True_ + +from ..bases import AstalonWorldBase from ..items import ( BlueDoor, Character, Crystal, Elevator, + Events, Eye, Face, KeyItem, @@ -12,118 +17,115 @@ WhiteDoor, ) from ..locations import LocationName as L +from ..options import ( + ApexElevator, + Difficulty, + RandomizeBlueKeys, + RandomizeCharacters, + RandomizeRedKeys, + RandomizeSwitches, + RandomizeWhiteKeys, +) from ..regions import RegionName as R -from .factories import ( - And, +from .custom_rules import ( CanReachEntrance, CanReachRegion, - False_, HardLogic, Has, HasAll, - HasAny, HasBlue, HasElevator, HasGoal, HasRed, HasSwitch, HasWhite, - Or, - RuleFactory, - True_, ) -easy = {"difficulty": 0} -characters_off = {"randomize_characters": 0} -characters_on = {"randomize_characters__ge": 1} -white_off = {"randomize_white_keys": 0} -blue_off = {"randomize_blue_keys": 0} -red_off = {"randomize_red_keys": 0} -switch_off = {"randomize_switches": 0} +easy = [OptionFilter(Difficulty, Difficulty.option_easy)] +characters_off = [OptionFilter(RandomizeCharacters, RandomizeCharacters.option_vanilla)] +characters_on = [OptionFilter(RandomizeCharacters, RandomizeCharacters.option_vanilla, operator="gt")] +white_off = [OptionFilter(RandomizeWhiteKeys, RandomizeWhiteKeys.option_false)] +blue_off = [OptionFilter(RandomizeBlueKeys, RandomizeBlueKeys.option_false)] +red_off = [OptionFilter(RandomizeRedKeys, RandomizeRedKeys.option_false)] +switch_off = [OptionFilter(RandomizeSwitches, RandomizeSwitches.option_false)] -true = True_() -false = False_() +has_algus = Has(Character.ALGUS, options=characters_on, filtered_resolution=True) +has_arias = Has(Character.ARIAS, options=characters_on, filtered_resolution=True) +has_kyuli = Has(Character.KYULI, options=characters_on, filtered_resolution=True) +has_bram = Has(Character.BRAM) +has_zeek = Has(Character.ZEEK) -can_uppies = HardLogic( - Or( - True_(options=characters_off), - HasAny(Character.ARIAS, Character.BRAM, options=characters_on), - ) -) -can_extra_height = Or(HasAny(Character.KYULI, KeyItem.BLOCK), can_uppies) -can_extra_height_gold_block = Or(HasAny(Character.KYULI, Character.ZEEK), can_uppies) -can_combo_height = And(can_uppies, HasAll(KeyItem.BELL, KeyItem.BLOCK)) -can_block_in_wall = HardLogic(HasAll(Character.ZEEK, KeyItem.BLOCK)) -can_crystal = Or( - HasAny(Character.ALGUS, KeyItem.BLOCK, ShopUpgrade.BRAM_WHIPLASH), - HasAll(Character.ZEEK, KeyItem.BANISH), - HardLogic(Has(ShopUpgrade.KYULI_RAY)), -) -can_crystal_wo_whiplash = Or( - HasAny(Character.ALGUS, KeyItem.BLOCK), - HasAll(Character.ZEEK, KeyItem.BANISH), - HardLogic(Has(ShopUpgrade.KYULI_RAY)), -) -can_crystal_wo_block = Or( - HasAny(Character.ALGUS, ShopUpgrade.BRAM_WHIPLASH), - HardLogic(Has(ShopUpgrade.KYULI_RAY)), -) -can_big_magic = HardLogic(HasAll(Character.ALGUS, KeyItem.BANISH, ShopUpgrade.ALGUS_ARCANIST)) -can_kill_ghosts = Or( - HasAny(KeyItem.BANISH, KeyItem.BLOCK), - HasAll(ShopUpgrade.ALGUS_METEOR, KeyItem.CHALICE, options=easy), - HardLogic(Has(ShopUpgrade.ALGUS_METEOR)), -) +has_cloak = has_algus & Has(KeyItem.CLOAK) +has_sword = has_arias & Has(KeyItem.SWORD) +has_boots = has_arias & Has(KeyItem.BOOTS) +has_claw = has_kyuli & Has(KeyItem.CLAW) +has_bow = has_kyuli & Has(KeyItem.BOW) +has_block = has_zeek & Has(KeyItem.BLOCK) +has_star = has_bram & Has(KeyItem.STAR) +has_banish = (has_algus | has_zeek) & Has(KeyItem.BANISH) +has_gauntlet = (has_arias | has_bram) & Has(KeyItem.GAUNTLET) +has_void = Has(KeyItem.VOID) -otherwise_crystal = Or( - HasAny(Character.ALGUS, KeyItem.BLOCK, ShopUpgrade.BRAM_WHIPLASH), - HasAll(Character.ZEEK, KeyItem.BANISH), - HardLogic(Has(ShopUpgrade.KYULI_RAY)), - options=switch_off, -) -otherwise_bow = Has(KeyItem.BOW, options=switch_off) -chalice_on_easy = Or(HardLogic(True_()), Has(KeyItem.CHALICE, options=easy)) +has_algus_arcanist = has_algus & Has(ShopUpgrade.ALGUS_ARCANIST) +has_algus_meteor = has_algus & Has(ShopUpgrade.ALGUS_METEOR) +has_algus_shock = has_algus & Has(ShopUpgrade.ALGUS_SHOCK) +has_arias_gorgonslayer = has_arias & Has(ShopUpgrade.ARIAS_GORGONSLAYER) +has_arias_last_stand = has_arias & Has(ShopUpgrade.ARIAS_LAST_STAND) +has_arias_lionheart = has_arias & Has(ShopUpgrade.ARIAS_LIONHEART) +has_kyuli_assassin = has_kyuli & Has(ShopUpgrade.KYULI_ASSASSIN) +has_kyuli_bullseye = has_kyuli & Has(ShopUpgrade.KYULI_BULLSEYE) +has_kyuli_ray = has_kyuli & Has(ShopUpgrade.KYULI_RAY) +has_zeek_junkyard = has_zeek & Has(ShopUpgrade.ZEEK_JUNKYARD) +has_zeek_orbs = has_zeek & Has(ShopUpgrade.ZEEK_ORBS) +has_zeek_loot = has_zeek & Has(ShopUpgrade.ZEEK_LOOT) +has_bram_axe = has_bram & Has(ShopUpgrade.BRAM_AXE) +has_bram_hunter = has_bram & Has(ShopUpgrade.BRAM_HUNTER) +has_bram_whiplash = has_bram & Has(ShopUpgrade.BRAM_WHIPLASH) +chalice_on_easy = Has(KeyItem.CHALICE, options=easy) | HardLogic(True_()) + +can_uppies = HardLogic(has_arias | has_bram) +can_extra_height = has_kyuli | has_block | can_uppies +can_extra_height_gold_block = has_kyuli | has_zeek | can_uppies +can_combo_height = can_uppies & has_block & Has(KeyItem.BELL) +can_block_in_wall = HardLogic(has_block) +can_crystal_limited = has_algus | HardLogic(has_kyuli_ray) +can_crystal_no_whiplash = can_crystal_limited | has_block | (has_zeek & has_banish) +can_crystal_no_block = can_crystal_limited | has_bram_whiplash +can_crystal = can_crystal_no_whiplash | has_bram_whiplash +can_big_magic = HardLogic(has_algus_arcanist & has_banish) +can_kill_ghosts = has_banish | has_block | (has_algus_meteor & chalice_on_easy) + +otherwise_crystal = can_crystal & switch_off +otherwise_bow = has_bow & switch_off -elevator_apex = Or( - HasElevator(Elevator.APEX, options={"apex_elevator": 1}), - Has(KeyItem.ASCENDANT_KEY, options={"apex_elevator": 0}), +elevator_apex = HasElevator( + Elevator.APEX, + options=[OptionFilter(ApexElevator, ApexElevator.option_included)], +) | Has( + KeyItem.ASCENDANT_KEY, + options=[OptionFilter(ApexElevator, ApexElevator.option_vanilla)], ) + # TODO: better implementations shop_cheap = CanReachRegion(R.GT_LEFT) shop_moderate = CanReachRegion(R.MECH_START) shop_expensive = CanReachRegion(R.ROA_START) -has_void = Has(KeyItem.VOID) - -MAIN_ENTRANCE_RULES: dict[tuple[R, R], RuleFactory] = { - (R.SHOP, R.SHOP_ALGUS): Has(Character.ALGUS), - (R.SHOP, R.SHOP_ARIAS): Has(Character.ARIAS), - (R.SHOP, R.SHOP_KYULI): Has(Character.KYULI), - (R.SHOP, R.SHOP_ZEEK): Has(Character.ZEEK), - (R.SHOP, R.SHOP_BRAM): Has(Character.BRAM), +MAIN_ENTRANCE_RULES: dict[tuple[R, R], Rule[AstalonWorldBase]] = { + (R.SHOP, R.SHOP_ALGUS): has_algus, + (R.SHOP, R.SHOP_ARIAS): has_arias, + (R.SHOP, R.SHOP_KYULI): has_kyuli, + (R.SHOP, R.SHOP_ZEEK): has_zeek, + (R.SHOP, R.SHOP_BRAM): has_bram, (R.GT_ENTRANCE, R.GT_BESTIARY): HasBlue(BlueDoor.GT_HUNTER, otherwise=True), - (R.GT_ENTRANCE, R.GT_BABY_GORGON): And( - Has(Eye.GREEN), - Or( - Has(KeyItem.CLAW), - HardLogic( - And( - Has(Character.ZEEK), - Or(HasAll(Character.KYULI, KeyItem.BELL), Has(KeyItem.BLOCK)), - ) - ), - ), - ), - (R.GT_ENTRANCE, R.GT_BOTTOM): Or( - HasSwitch(Switch.GT_2ND_ROOM), - HasWhite(WhiteDoor.GT_START, otherwise=True, options=switch_off), + (R.GT_ENTRANCE, R.GT_BABY_GORGON): ( + Has(Eye.GREEN) & (has_claw | HardLogic(has_zeek & ((has_kyuli & Has(KeyItem.BELL)) | has_block))) ), - (R.GT_ENTRANCE, R.GT_VOID): has_void, - (R.GT_ENTRANCE, R.GT_GORGONHEART): Or( - HasSwitch(Switch.GT_GH_SHORTCUT), - Has(KeyItem.BOOTS), - HardLogic(Has(KeyItem.ICARUS)), + (R.GT_ENTRANCE, R.GT_BOTTOM): ( + HasSwitch(Switch.GT_2ND_ROOM) | HasWhite(WhiteDoor.GT_START, otherwise=True, options=switch_off) ), + (R.GT_ENTRANCE, R.GT_VOID): has_void, + (R.GT_ENTRANCE, R.GT_GORGONHEART): HasSwitch(Switch.GT_GH_SHORTCUT) | has_boots | HardLogic(Has(KeyItem.ICARUS)), (R.GT_ENTRANCE, R.GT_BOSS): HasElevator(Elevator.GT_2), (R.GT_ENTRANCE, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.GT_ENTRANCE, R.MECH_BOSS): HasElevator(Elevator.MECH_2), @@ -137,53 +139,34 @@ (R.GT_BOTTOM, R.GT_ENTRANCE): HasSwitch(Switch.GT_2ND_ROOM), (R.GT_BOTTOM, R.GT_VOID): Has(Eye.RED), (R.GT_BOTTOM, R.GT_GORGONHEART): HasWhite(WhiteDoor.GT_MAP, otherwise=True), - (R.GT_BOTTOM, R.GT_UPPER_PATH): Or( - HasSwitch(Crystal.GT_ROTA), - can_uppies, - And(Has(KeyItem.STAR), HasBlue(BlueDoor.GT_RING, otherwise=True)), - Has(KeyItem.BLOCK), - ), - (R.GT_BOTTOM, R.CAVES_START): Or( - Has(Character.KYULI), - HardLogic(HasAny(Character.ZEEK, KeyItem.BOOTS)), + (R.GT_BOTTOM, R.GT_UPPER_PATH): ( + HasSwitch(Crystal.GT_ROTA) | can_uppies | (has_star & HasBlue(BlueDoor.GT_RING, otherwise=True)) | has_block ), + (R.GT_BOTTOM, R.CAVES_START): has_kyuli | HardLogic(has_zeek | has_boots), (R.GT_VOID, R.GT_ENTRANCE): has_void, (R.GT_VOID, R.GT_BOTTOM): Has(Eye.RED), (R.GT_VOID, R.MECH_SNAKE): HasSwitch(Switch.MECH_SNAKE_2), (R.GT_GORGONHEART, R.GT_ORBS_DOOR): HasBlue(BlueDoor.GT_ORBS, otherwise=True), - (R.GT_GORGONHEART, R.GT_LEFT): Or( - HasSwitch(Switch.GT_CROSSES), - HasSwitch(Switch.GT_1ST_CYCLOPS, otherwise=True), - ), - (R.GT_LEFT, R.GT_GORGONHEART): Or( - HasSwitch(Switch.GT_CROSSES, otherwise=True), - HasSwitch(Switch.GT_1ST_CYCLOPS), - ), + (R.GT_GORGONHEART, R.GT_LEFT): HasSwitch(Switch.GT_CROSSES) | HasSwitch(Switch.GT_1ST_CYCLOPS, otherwise=True), + (R.GT_LEFT, R.GT_GORGONHEART): HasSwitch(Switch.GT_CROSSES, otherwise=True) | HasSwitch(Switch.GT_1ST_CYCLOPS), (R.GT_LEFT, R.GT_ORBS_HEIGHT): can_extra_height, (R.GT_LEFT, R.GT_ASCENDANT_KEY): HasBlue(BlueDoor.GT_ASCENDANT, otherwise=True), - (R.GT_LEFT, R.GT_TOP_LEFT): Or( - HasSwitch(Switch.GT_ARIAS), - HasAny(Character.ARIAS, KeyItem.CLAW), - HasAll(KeyItem.BLOCK, Character.KYULI, KeyItem.BELL), + (R.GT_LEFT, R.GT_TOP_LEFT): ( + HasSwitch(Switch.GT_ARIAS) | has_arias | has_claw | (has_block & has_kyuli & Has(KeyItem.BELL)) ), (R.GT_LEFT, R.GT_TOP_RIGHT): can_extra_height, - (R.GT_TOP_LEFT, R.GT_BUTT): Or( - HasSwitch(Switch.GT_BUTT_ACCESS), - CanReachRegion(R.GT_SPIKE_TUNNEL_SWITCH, options=switch_off), + (R.GT_TOP_LEFT, R.GT_BUTT): ( + HasSwitch(Switch.GT_BUTT_ACCESS) | CanReachRegion(R.GT_SPIKE_TUNNEL_SWITCH, options=switch_off) ), - (R.GT_TOP_RIGHT, R.GT_SPIKE_TUNNEL): Or( - HasSwitch(Switch.GT_SPIKE_TUNNEL), - CanReachRegion(R.GT_TOP_LEFT, options=switch_off), + (R.GT_TOP_RIGHT, R.GT_SPIKE_TUNNEL): ( + HasSwitch(Switch.GT_SPIKE_TUNNEL) | CanReachRegion(R.GT_TOP_LEFT, options=switch_off) ), (R.GT_SPIKE_TUNNEL, R.GT_TOP_RIGHT): HasSwitch(Switch.GT_SPIKE_TUNNEL), (R.GT_SPIKE_TUNNEL, R.GT_SPIKE_TUNNEL_SWITCH): can_extra_height, - (R.GT_SPIKE_TUNNEL_SWITCH, R.GT_BUTT): Or( - HardLogic(Has(KeyItem.STAR)), - HasAll(KeyItem.STAR, KeyItem.BELL, options=easy), - ), + (R.GT_SPIKE_TUNNEL_SWITCH, R.GT_BUTT): HardLogic(has_star) | Filtered(has_star & Has(KeyItem.BELL), options=easy), (R.GT_BUTT, R.GT_TOP_LEFT): HasSwitch(Switch.GT_BUTT_ACCESS), - (R.GT_BUTT, R.GT_SPIKE_TUNNEL_SWITCH): Has(KeyItem.STAR), - (R.GT_BUTT, R.GT_BOSS): Or(HasWhite(WhiteDoor.GT_TAUROS), CanReachRegion(R.GT_TOP_RIGHT, options=white_off)), + (R.GT_BUTT, R.GT_SPIKE_TUNNEL_SWITCH): has_star, + (R.GT_BUTT, R.GT_BOSS): HasWhite(WhiteDoor.GT_TAUROS) | CanReachRegion(R.GT_TOP_RIGHT, options=white_off), (R.GT_BOSS, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), (R.GT_BOSS, R.GT_BUTT): HasWhite(WhiteDoor.GT_TAUROS), (R.GT_BOSS, R.MECH_START): Has(Eye.RED), @@ -196,75 +179,65 @@ (R.GT_BOSS, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.GT_BOSS, R.CATA_BOSS): HasElevator(Elevator.CATA_2), (R.GT_BOSS, R.TR_START): HasElevator(Elevator.TR), - (R.GT_UPPER_ARIAS, R.GT_OLD_MAN_FORK): Or( - HasSwitch(Crystal.GT_LADDER), - CanReachRegion(R.GT_LADDER_SWITCH, options=switch_off), + (R.GT_UPPER_ARIAS, R.GT_OLD_MAN_FORK): ( + HasSwitch(Crystal.GT_LADDER) | CanReachRegion(R.GT_LADDER_SWITCH, options=switch_off) ), - (R.GT_UPPER_ARIAS, R.MECH_SWORD_CONNECTION): Or( - Has(Character.ARIAS), - HasSwitch(Switch.GT_UPPER_ARIAS), - CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off), + (R.GT_UPPER_ARIAS, R.MECH_SWORD_CONNECTION): ( + has_arias | HasSwitch(Switch.GT_UPPER_ARIAS) | CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off) ), - (R.GT_OLD_MAN_FORK, R.GT_UPPER_ARIAS): Or( - HasSwitch(Crystal.GT_LADDER), - CanReachRegion(R.GT_LADDER_SWITCH, options=switch_off), + (R.GT_OLD_MAN_FORK, R.GT_UPPER_ARIAS): ( + HasSwitch(Crystal.GT_LADDER) | CanReachRegion(R.GT_LADDER_SWITCH, options=switch_off) ), (R.GT_OLD_MAN_FORK, R.GT_SWORD_FORK): HasBlue(BlueDoor.GT_SWORD, otherwise=True), - (R.GT_OLD_MAN_FORK, R.GT_OLD_MAN): Or( - Has(KeyItem.CLAW), + (R.GT_OLD_MAN_FORK, R.GT_OLD_MAN): ( # TODO: you don't need both switches, revisit when adding old man - HasSwitch(Crystal.GT_OLD_MAN_1, Crystal.GT_OLD_MAN_2), - otherwise_crystal, + has_claw | HasSwitch(Crystal.GT_OLD_MAN_1, Crystal.GT_OLD_MAN_2) | otherwise_crystal ), (R.GT_SWORD_FORK, R.GT_SWORD): HasSwitch(Switch.GT_SWORD_ACCESS, otherwise=True), - (R.GT_SWORD_FORK, R.GT_ARIAS_SWORD_SWITCH): Or(Has(KeyItem.SWORD), HasAll(KeyItem.BOW, KeyItem.BELL)), + (R.GT_SWORD_FORK, R.GT_ARIAS_SWORD_SWITCH): has_sword | (has_bow & Has(KeyItem.BELL)), (R.GT_UPPER_PATH, R.GT_UPPER_PATH_CONNECTION): HasSwitch(Switch.GT_UPPER_PATH_ACCESS), (R.GT_UPPER_PATH_CONNECTION, R.GT_UPPER_PATH): HasSwitch(Switch.GT_UPPER_PATH_ACCESS, otherwise=True), (R.GT_UPPER_PATH_CONNECTION, R.MECH_SWORD_CONNECTION): HasSwitch(Switch.MECH_TO_UPPER_GT), (R.GT_UPPER_PATH_CONNECTION, R.MECH_BOTTOM_CAMPFIRE): HasSwitch(Switch.MECH_TO_UPPER_GT), - (R.MECH_START, R.GT_LADDER_SWITCH): And(Has(Eye.RED), can_crystal), - (R.MECH_START, R.MECH_BK): And(HasBlue(BlueDoor.MECH_SHORTCUT, otherwise=True), can_extra_height), - (R.MECH_START, R.MECH_WATCHER): And( - Or(HasSwitch(Switch.MECH_CANNON), otherwise_crystal), - Or( - HasWhite(WhiteDoor.MECH_2ND), - And( - CanReachRegion(R.MECH_SWORD_CONNECTION), - HasSwitch(Switch.MECH_LOWER_KEY, otherwise=True), + (R.MECH_START, R.GT_LADDER_SWITCH): Has(Eye.RED) & can_crystal, + (R.MECH_START, R.MECH_BK): HasBlue(BlueDoor.MECH_SHORTCUT, otherwise=True) & can_extra_height, + (R.MECH_START, R.MECH_WATCHER): ( + (HasSwitch(Switch.MECH_CANNON) | otherwise_crystal) + & ( + HasWhite(WhiteDoor.MECH_2ND) + | Filtered( + CanReachRegion(R.MECH_SWORD_CONNECTION) & HasSwitch(Switch.MECH_LOWER_KEY, otherwise=True), options=white_off, - ), - ), + ) + ) ), - (R.MECH_START, R.MECH_LINUS): Or(HasSwitch(Crystal.MECH_LINUS), otherwise_crystal), + (R.MECH_START, R.MECH_LINUS): HasSwitch(Crystal.MECH_LINUS) | otherwise_crystal, (R.MECH_START, R.MECH_LOWER_VOID): HasBlue(BlueDoor.MECH_RED, otherwise=True), (R.MECH_START, R.MECH_SACRIFICE): can_extra_height, (R.MECH_START, R.GT_BOSS): Has(Eye.RED), - (R.MECH_LINUS, R.MECH_START): Or(HasSwitch(Crystal.MECH_LINUS), otherwise_crystal), + (R.MECH_LINUS, R.MECH_START): HasSwitch(Crystal.MECH_LINUS) | otherwise_crystal, (R.MECH_LINUS, R.MECH_SWORD_CONNECTION): HasSwitch(Switch.MECH_LINUS, otherwise=True), - (R.MECH_SWORD_CONNECTION, R.MECH_BOOTS_CONNECTION): And( - HasBlue(BlueDoor.MECH_BOOTS, otherwise=True), - Or( - HasSwitch(Crystal.MECH_LOWER), - otherwise_crystal, - HasAny(KeyItem.CLAW, KeyItem.CLOAK), - HasAll(Character.KYULI, KeyItem.ICARUS), - HardLogic(Has(KeyItem.BOOTS)), - ), + (R.MECH_SWORD_CONNECTION, R.MECH_BOOTS_CONNECTION): ( + HasBlue(BlueDoor.MECH_BOOTS, otherwise=True) + & ( + HasSwitch(Crystal.MECH_LOWER) + | otherwise_crystal + | has_claw + | has_cloak + | (has_kyuli & Has(KeyItem.ICARUS)) + | HardLogic(has_boots) + ) ), (R.MECH_SWORD_CONNECTION, R.GT_UPPER_PATH_CONNECTION): HasSwitch(Switch.MECH_TO_UPPER_GT), - (R.MECH_SWORD_CONNECTION, R.MECH_LOWER_ARIAS): Has(Character.ARIAS), + (R.MECH_SWORD_CONNECTION, R.MECH_LOWER_ARIAS): has_arias, (R.MECH_SWORD_CONNECTION, R.MECH_BOTTOM_CAMPFIRE): HasSwitch(Switch.MECH_TO_UPPER_GT), (R.MECH_SWORD_CONNECTION, R.MECH_LINUS): HasSwitch(Switch.MECH_LINUS), - (R.MECH_SWORD_CONNECTION, R.GT_UPPER_ARIAS): Or(Has(Character.ARIAS), HasSwitch(Switch.GT_UPPER_ARIAS)), + (R.MECH_SWORD_CONNECTION, R.GT_UPPER_ARIAS): has_arias | HasSwitch(Switch.GT_UPPER_ARIAS), (R.MECH_BOOTS_CONNECTION, R.MECH_BOTTOM_CAMPFIRE): HasBlue(BlueDoor.MECH_VOID, otherwise=True), - (R.MECH_BOOTS_CONNECTION, R.MECH_BOOTS_LOWER): Or( - HasSwitch(Switch.MECH_BOOTS), - HasAny(Eye.RED, KeyItem.STAR, options=switch_off), - ), - (R.MECH_BOOTS_LOWER, R.MECH_BOOTS_UPPER): Or( - HasSwitch(Switch.MECH_BOOTS_LOWER, otherwise=True), - can_extra_height, + (R.MECH_BOOTS_CONNECTION, R.MECH_BOOTS_LOWER): ( + HasSwitch(Switch.MECH_BOOTS) | Filtered(Has(Eye.RED) | has_star, options=switch_off) ), + (R.MECH_BOOTS_LOWER, R.MECH_BOOTS_UPPER): HasSwitch(Switch.MECH_BOOTS_LOWER, otherwise=True) | can_extra_height, (R.MECH_BOTTOM_CAMPFIRE, R.GT_UPPER_PATH_CONNECTION): HasSwitch(Switch.MECH_TO_UPPER_GT, otherwise=True), (R.MECH_BOTTOM_CAMPFIRE, R.MECH_BOOTS_CONNECTION): HasBlue(BlueDoor.MECH_VOID, otherwise=True), (R.MECH_BOTTOM_CAMPFIRE, R.MECH_SNAKE): HasSwitch(Switch.MECH_SNAKE_1, otherwise=True), @@ -274,174 +247,128 @@ (R.MECH_LOWER_VOID, R.MECH_START): HasBlue(BlueDoor.MECH_RED, otherwise=True), (R.MECH_LOWER_VOID, R.MECH_UPPER_VOID): has_void, (R.MECH_LOWER_VOID, R.HOTP_MECH_VOID_CONNECTION): Has(Eye.BLUE), - (R.MECH_WATCHER, R.MECH_START): And( - HasSwitch(Switch.MECH_CANNON), - HasWhite(WhiteDoor.MECH_2ND), - ), - (R.MECH_WATCHER, R.MECH_ROOTS): Or(Has(KeyItem.CLAW), HasSwitch(Switch.MECH_WATCHER, otherwise=True)), - (R.MECH_ROOTS, R.MECH_ZEEK_CONNECTION): HasAll(KeyItem.CLAW, KeyItem.BLOCK, KeyItem.BELL), + (R.MECH_WATCHER, R.MECH_START): HasSwitch(Switch.MECH_CANNON) & HasWhite(WhiteDoor.MECH_2ND), + (R.MECH_WATCHER, R.MECH_ROOTS): has_claw | HasSwitch(Switch.MECH_WATCHER, otherwise=True), + (R.MECH_ROOTS, R.MECH_ZEEK_CONNECTION): has_claw & has_block & Has(KeyItem.BELL), (R.MECH_ROOTS, R.MECH_MUSIC): HasBlue(BlueDoor.MECH_MUSIC, otherwise=True), - (R.MECH_BK, R.MECH_START): And( - HasBlue(BlueDoor.MECH_SHORTCUT, otherwise=True), - Or(Has(Character.KYULI), can_combo_height), - ), - (R.MECH_BK, R.MECH_AFTER_BK): Or(HasSwitch(Crystal.MECH_BK), otherwise_crystal), - (R.MECH_BK, R.MECH_ROOTS): Or(HasSwitch(Crystal.MECH_CAMPFIRE), otherwise_crystal), - (R.MECH_BK, R.MECH_TRIPLE_SWITCHES): And( - can_crystal, - HasSwitch( + (R.MECH_BK, R.MECH_START): HasBlue(BlueDoor.MECH_SHORTCUT, otherwise=True) & (has_kyuli | can_combo_height), + (R.MECH_BK, R.MECH_AFTER_BK): HasSwitch(Crystal.MECH_BK) | otherwise_crystal, + (R.MECH_BK, R.MECH_ROOTS): HasSwitch(Crystal.MECH_CAMPFIRE) | otherwise_crystal, + (R.MECH_BK, R.MECH_TRIPLE_SWITCHES): ( + can_crystal + & HasSwitch( Crystal.MECH_BK, Switch.MECH_TO_BOSS_1, Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3, - ), - Or(HasWhite(WhiteDoor.MECH_BK), HasSwitch(Switch.MECH_CHAINS)), - ), - (R.MECH_AFTER_BK, R.MECH_CHAINS_CANDLE): Or( - Has(KeyItem.CLAW), - HasWhite(WhiteDoor.MECH_BK, otherwise=True), + ) + & (HasWhite(WhiteDoor.MECH_BK) | HasSwitch(Switch.MECH_CHAINS)) ), + (R.MECH_AFTER_BK, R.MECH_CHAINS_CANDLE): has_claw | HasWhite(WhiteDoor.MECH_BK, otherwise=True), (R.MECH_AFTER_BK, R.MECH_CHAINS): HasSwitch(Switch.MECH_CHAINS), - (R.MECH_AFTER_BK, R.MECH_BK): Or( - HasSwitch(Crystal.MECH_BK), - HardLogic(Has(ShopUpgrade.KYULI_RAY), options=switch_off), - ), - (R.MECH_AFTER_BK, R.HOTP_EPIMETHEUS): Has(KeyItem.CLAW), - (R.MECH_CHAINS, R.MECH_CHAINS_CANDLE): Has(KeyItem.CLAW), - (R.MECH_CHAINS, R.MECH_ARIAS_EYEBALL): Has(Character.ARIAS), + (R.MECH_AFTER_BK, R.MECH_BK): HasSwitch(Crystal.MECH_BK) | HardLogic(has_kyuli_ray, options=switch_off), + (R.MECH_AFTER_BK, R.HOTP_EPIMETHEUS): has_claw, + (R.MECH_CHAINS, R.MECH_CHAINS_CANDLE): has_claw, + (R.MECH_CHAINS, R.MECH_ARIAS_EYEBALL): has_arias, (R.MECH_CHAINS, R.MECH_SPLIT_PATH): HasSwitch(Switch.MECH_SPLIT_PATH, otherwise=True), (R.MECH_CHAINS, R.MECH_BOSS_SWITCHES): HasSwitch(Switch.MECH_TO_BOSS_1), - (R.MECH_CHAINS, R.MECH_BOSS_CONNECTION): Or( - Has(KeyItem.CLAW), - HasSwitch(Switch.MECH_TO_BOSS_2), - HasSwitch(Crystal.MECH_TO_BOSS_3), - HardLogic(Or(can_big_magic, Has(ShopUpgrade.KYULI_RAY)), options=switch_off), + (R.MECH_CHAINS, R.MECH_BOSS_CONNECTION): ( + has_claw + | HasSwitch(Switch.MECH_TO_BOSS_2) + | HasSwitch(Crystal.MECH_TO_BOSS_3) + | HardLogic(can_big_magic | has_kyuli_ray, options=switch_off) ), (R.MECH_CHAINS, R.MECH_AFTER_BK): HasSwitch(Switch.MECH_CHAINS, otherwise=True), - (R.MECH_ARIAS_EYEBALL, R.MECH_ZEEK_CONNECTION): Or( - HasSwitch(Switch.MECH_ARIAS, otherwise=True), - HasAll(KeyItem.STAR, KeyItem.BELL), + (R.MECH_ARIAS_EYEBALL, R.MECH_ZEEK_CONNECTION): ( + HasSwitch(Switch.MECH_ARIAS, otherwise=True) | (has_star & Has(KeyItem.BELL)) ), - (R.MECH_ARIAS_EYEBALL, R.MECH_CHAINS): And( - HasAll(Character.ARIAS, KeyItem.BELL), - HasAny(Character.ALGUS, ShopUpgrade.BRAM_WHIPLASH), - Or(HasSwitch(Switch.MECH_ARIAS), Has(KeyItem.STAR)), + (R.MECH_ARIAS_EYEBALL, R.MECH_CHAINS): ( + has_arias & Has(KeyItem.BELL) & (has_algus | has_bram_whiplash) & (HasSwitch(Switch.MECH_ARIAS) | has_star) ), (R.MECH_ZEEK_CONNECTION, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), - (R.MECH_ZEEK_CONNECTION, R.MECH_ARIAS_EYEBALL): Or( - HasSwitch(Switch.MECH_ARIAS), - HasAll(KeyItem.STAR, Character.ARIAS), - ), + (R.MECH_ZEEK_CONNECTION, R.MECH_ARIAS_EYEBALL): HasSwitch(Switch.MECH_ARIAS) | (has_star & has_arias), (R.MECH_ZEEK_CONNECTION, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.MECH_ZEEK_CONNECTION, R.CATA_BOSS): HasElevator(Elevator.CATA_2), (R.MECH_ZEEK_CONNECTION, R.HOTP_ELEVATOR): HasElevator(Elevator.HOTP), (R.MECH_ZEEK_CONNECTION, R.TR_START): HasElevator(Elevator.TR), (R.MECH_ZEEK_CONNECTION, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.MECH_ZEEK_CONNECTION, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), - (R.MECH_ZEEK_CONNECTION, R.MECH_ZEEK): Or( - HasRed(RedDoor.ZEEK), - CanReachRegion(R.MECH_LOWER_VOID, options=red_off), - ), + (R.MECH_ZEEK_CONNECTION, R.MECH_ZEEK): HasRed(RedDoor.ZEEK) | CanReachRegion(R.MECH_LOWER_VOID, options=red_off), (R.MECH_ZEEK_CONNECTION, R.APEX): elevator_apex, (R.MECH_ZEEK_CONNECTION, R.GT_BOSS): HasElevator(Elevator.GT_2), (R.MECH_ZEEK_CONNECTION, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.MECH_SPLIT_PATH, R.MECH_CHAINS): HasSwitch(Switch.MECH_SPLIT_PATH), + (R.MECH_SPLIT_PATH, R.MECH_RIGHT): HasSwitch(Switch.MECH_SKULL_PUZZLE, otherwise=True), (R.MECH_RIGHT, R.MECH_TRIPLE_SWITCHES): HardLogic( - And( - HasSwitch( - Switch.MECH_SPLIT_PATH, - Switch.MECH_TO_BOSS_1, - Crystal.MECH_TRIPLE_1, - Crystal.MECH_TRIPLE_2, - Crystal.MECH_TRIPLE_3, - ), - HasAll(KeyItem.STAR, ShopUpgrade.BRAM_WHIPLASH), + HasSwitch( + Switch.MECH_SPLIT_PATH, + Switch.MECH_TO_BOSS_1, + Crystal.MECH_TRIPLE_1, + Crystal.MECH_TRIPLE_2, + Crystal.MECH_TRIPLE_3, ) + & has_star + & has_bram_whiplash ), - (R.MECH_RIGHT, R.MECH_OLD_MAN): Or( - HasSwitch(Crystal.MECH_OLD_MAN), - otherwise_crystal, - HasAll(Character.KYULI, KeyItem.BLOCK, KeyItem.BELL), + (R.MECH_RIGHT, R.MECH_OLD_MAN): ( + HasSwitch(Crystal.MECH_OLD_MAN) | otherwise_crystal | (has_kyuli & has_block & Has(KeyItem.BELL)) ), - (R.MECH_RIGHT, R.MECH_SPLIT_PATH): Has(KeyItem.STAR), - (R.MECH_RIGHT, R.MECH_BELOW_POTS): Or( - HasWhite(WhiteDoor.MECH_ARENA, otherwise=True), - HasSwitch(Switch.MECH_EYEBALL), + (R.MECH_RIGHT, R.MECH_SPLIT_PATH): has_star | HasSwitch(Switch.MECH_SKULL_PUZZLE), + (R.MECH_RIGHT, R.MECH_BELOW_POTS): ( + HasWhite(WhiteDoor.MECH_ARENA, otherwise=True) | HasSwitch(Switch.MECH_EYEBALL) ), - (R.MECH_RIGHT, R.MECH_UPPER_VOID): Or( - HasSwitch(Switch.MECH_UPPER_VOID), - And(Has(KeyItem.CLAW), HasSwitch(Switch.MECH_UPPER_VOID_DROP, otherwise=True)), + (R.MECH_RIGHT, R.MECH_UPPER_VOID): ( + HasSwitch(Switch.MECH_UPPER_VOID) | (has_claw & HasSwitch(Switch.MECH_UPPER_VOID_DROP, otherwise=True)) ), (R.MECH_UPPER_VOID, R.MECH_RIGHT): HasSwitch(Switch.MECH_UPPER_VOID, otherwise=True), (R.MECH_UPPER_VOID, R.MECH_LOWER_VOID): has_void, - (R.MECH_BELOW_POTS, R.MECH_RIGHT): Or( - HasWhite(WhiteDoor.MECH_ARENA), - HasSwitch(Switch.MECH_EYEBALL, otherwise=True), + (R.MECH_BELOW_POTS, R.MECH_RIGHT): ( + HasWhite(WhiteDoor.MECH_ARENA) | HasSwitch(Switch.MECH_EYEBALL, otherwise=True) ), (R.MECH_BELOW_POTS, R.MECH_POTS): HasSwitch(Switch.MECH_POTS, otherwise=True), (R.MECH_POTS, R.MECH_BELOW_POTS): HasSwitch(Switch.MECH_POTS), (R.MECH_POTS, R.MECH_TOP): HasSwitch(Switch.MECH_POTS, otherwise=True), (R.MECH_TOP, R.MECH_POTS): HasSwitch(Switch.MECH_POTS), - (R.MECH_TOP, R.MECH_TP_CONNECTION): Or( - Has(KeyItem.CLAW), - Or( - HasWhite(WhiteDoor.MECH_TOP), - And(can_extra_height, Or(HasSwitch(Crystal.MECH_TOP), otherwise_crystal), options=white_off), - ), - ), - (R.MECH_TOP, R.MECH_CD_ACCESS): And( - Has(Eye.BLUE), - HasBlue(BlueDoor.MECH_CD, otherwise=True), - Or( - HasSwitch(Crystal.MECH_TO_CD), - otherwise_crystal, - HasAll(Character.KYULI, KeyItem.BLOCK, KeyItem.BELL), - ), + (R.MECH_TOP, R.MECH_TP_CONNECTION): ( + has_claw + | HasWhite(WhiteDoor.MECH_TOP) + | Filtered(can_extra_height & (HasSwitch(Crystal.MECH_TOP) | otherwise_crystal), options=white_off) + ), + (R.MECH_TOP, R.MECH_CD_ACCESS): ( + Has(Eye.BLUE) + & HasBlue(BlueDoor.MECH_CD, otherwise=True) + & (HasSwitch(Crystal.MECH_TO_CD) | otherwise_crystal | (has_kyuli & has_block & Has(KeyItem.BELL))) ), (R.MECH_CD_ACCESS, R.CD_START): Has(KeyItem.CYCLOPS), - (R.MECH_TOP, R.MECH_TRIPLE_SWITCHES): And( - can_crystal, - Or(HasSwitch(Switch.MECH_ARIAS_CYCLOPS), Has(Character.ARIAS, options=switch_off)), - Or( - HasWhite(WhiteDoor.MECH_TOP), - And(can_extra_height, Or(HasSwitch(Crystal.MECH_TOP), otherwise_crystal), options=white_off), - HasAll(KeyItem.CLAW, KeyItem.BELL), - ), - ), - (R.MECH_TP_CONNECTION, R.HOTP_FALL_BOTTOM): Or(Has(KeyItem.CLAW), HasSwitch(Switch.MECH_MAZE_BACKDOOR)), - (R.MECH_TP_CONNECTION, R.MECH_TOP): Or(Has(KeyItem.CLAW), HasWhite(WhiteDoor.MECH_TOP)), - (R.MECH_TP_CONNECTION, R.MECH_CHARACTER_SWAPS): Or( - And( - Has(Character.ARIAS), - Or(HasWhite(WhiteDoor.MECH_TOP, otherwise=True), Has(KeyItem.BELL)), - ), - HasSwitch(Switch.MECH_ARIAS_CYCLOPS), - ), - (R.MECH_CHARACTER_SWAPS, R.MECH_CLOAK_CONNECTION): And( - Or( - HasSwitch(Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3), - otherwise_crystal, - ), - can_extra_height, - ), - (R.MECH_CHARACTER_SWAPS, R.MECH_TP_CONNECTION): Or( - Has(Character.ARIAS), - HasSwitch(Switch.MECH_ARIAS_CYCLOPS, otherwise=True), + (R.MECH_TOP, R.MECH_TRIPLE_SWITCHES): ( + can_crystal + & (HasSwitch(Switch.MECH_ARIAS_CYCLOPS) | (has_arias & switch_off)) + & ( + HasWhite(WhiteDoor.MECH_TOP) + | Filtered(can_extra_height & (HasSwitch(Crystal.MECH_TOP) | otherwise_crystal), options=white_off) + | (has_claw & Has(KeyItem.BELL)) + ) + ), + (R.MECH_TP_CONNECTION, R.HOTP_FALL_BOTTOM): has_claw | HasSwitch(Switch.MECH_MAZE_BACKDOOR), + (R.MECH_TP_CONNECTION, R.MECH_TOP): has_claw | HasWhite(WhiteDoor.MECH_TOP), + (R.MECH_TP_CONNECTION, R.MECH_CHARACTER_SWAPS): ( + (has_arias & (HasWhite(WhiteDoor.MECH_TOP, otherwise=True) | Has(KeyItem.BELL))) + | HasSwitch(Switch.MECH_ARIAS_CYCLOPS) ), + (R.MECH_CHARACTER_SWAPS, R.MECH_CLOAK_CONNECTION): ( + (HasSwitch(Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3) | otherwise_crystal) + & can_extra_height + ), + (R.MECH_CHARACTER_SWAPS, R.MECH_TP_CONNECTION): has_arias | HasSwitch(Switch.MECH_ARIAS_CYCLOPS, otherwise=True), (R.MECH_CLOAK_CONNECTION, R.MECH_CHARACTER_SWAPS): HasSwitch( Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3, ), - (R.MECH_CLOAK_CONNECTION, R.MECH_CLOAK): And( - Has(Eye.BLUE), - Or(HasSwitch(Crystal.MECH_CLOAK), otherwise_crystal), - ), - (R.MECH_BOSS_SWITCHES, R.MECH_CLOAK_CONNECTION): Or( - HasSwitch(Switch.MECH_BLOCK_STAIRS), - HasSwitch(Crystal.MECH_SLIMES), - otherwise_crystal, + (R.MECH_CLOAK_CONNECTION, R.MECH_CLOAK): Has(Eye.BLUE) & (HasSwitch(Crystal.MECH_CLOAK) | otherwise_crystal), + (R.MECH_BOSS_SWITCHES, R.MECH_CLOAK_CONNECTION): ( + HasSwitch(Switch.MECH_BLOCK_STAIRS) | HasSwitch(Crystal.MECH_SLIMES) | otherwise_crystal ), (R.MECH_BOSS_SWITCHES, R.MECH_CHAINS): HasSwitch(Switch.MECH_TO_BOSS_1, otherwise=True), (R.MECH_BOSS_SWITCHES, R.MECH_BOSS_CONNECTION): HasSwitch( @@ -449,22 +376,21 @@ Switch.MECH_TO_BOSS_2, otherwise=True, ), - (R.MECH_BOSS_CONNECTION, R.MECH_BOSS): Or( - HasSwitch(Switch.MECH_BOSS_2, otherwise=True), - And(HasAll(KeyItem.BLOCK, KeyItem.BELL), Or(Has(Character.KYULI), can_uppies)), + (R.MECH_BOSS_CONNECTION, R.MECH_BOSS): ( + HasSwitch(Switch.MECH_BOSS_2, otherwise=True) | (has_block & Has(KeyItem.BELL) & (has_kyuli | can_uppies)) ), (R.MECH_BOSS_CONNECTION, R.MECH_BRAM_TUNNEL_CONNECTION): HasSwitch(Switch.MECH_BOSS_1, otherwise=True), (R.MECH_BRAM_TUNNEL_CONNECTION, R.MECH_BOSS_CONNECTION): HasSwitch(Switch.MECH_BOSS_1), - (R.MECH_BRAM_TUNNEL_CONNECTION, R.MECH_BRAM_TUNNEL): Has(KeyItem.STAR), - (R.MECH_BRAM_TUNNEL, R.MECH_BRAM_TUNNEL_CONNECTION): Has(KeyItem.STAR), - (R.MECH_BRAM_TUNNEL, R.HOTP_START_BOTTOM): Has(KeyItem.STAR), + (R.MECH_BRAM_TUNNEL_CONNECTION, R.MECH_BRAM_TUNNEL): has_star, + (R.MECH_BRAM_TUNNEL, R.MECH_BRAM_TUNNEL_CONNECTION): has_star, + (R.MECH_BRAM_TUNNEL, R.HOTP_START_BOTTOM): has_star, (R.MECH_BOSS, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), (R.MECH_BOSS, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.MECH_BOSS, R.CATA_BOSS): HasElevator(Elevator.CATA_2), (R.MECH_BOSS, R.TR_START): HasElevator(Elevator.TR), - (R.MECH_BOSS, R.MECH_TRIPLE_SWITCHES): And( - can_crystal, - HasSwitch(Switch.MECH_TO_BOSS_1, Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3), + (R.MECH_BOSS, R.MECH_TRIPLE_SWITCHES): ( + can_crystal + & HasSwitch(Switch.MECH_TO_BOSS_1, Crystal.MECH_TRIPLE_1, Crystal.MECH_TRIPLE_2, Crystal.MECH_TRIPLE_3) ), (R.MECH_BOSS, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.MECH_BOSS, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), @@ -474,111 +400,87 @@ (R.MECH_BOSS, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.MECH_BOSS, R.HOTP_ELEVATOR): HasElevator(Elevator.HOTP), (R.HOTP_START, R.MECH_BOSS): Has(Eye.BLUE), - (R.HOTP_START, R.HOTP_START_BOTTOM): Or( - Has(KeyItem.STAR), - And( - Or(HasWhite(WhiteDoor.HOTP_START), CanReachRegion(R.HOTP_START_LEFT, options=white_off)), - Has(Eye.BLUE), - ), + (R.HOTP_START, R.HOTP_START_BOTTOM): ( + has_star + | (Has(Eye.BLUE) & (HasWhite(WhiteDoor.HOTP_START) | CanReachRegion(R.HOTP_START_LEFT, options=white_off))) ), (R.HOTP_START, R.HOTP_START_MID): HasSwitch(Switch.HOTP_1ST_ROOM, otherwise=True), - (R.HOTP_START_MID, R.HOTP_START_LEFT): Or( - HasSwitch(Switch.HOTP_LEFT_3, otherwise=True), - And(Has(KeyItem.STAR), HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True)), + (R.HOTP_START_MID, R.HOTP_START_LEFT): ( + HasSwitch(Switch.HOTP_LEFT_3, otherwise=True) + | (has_star & HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True)) ), (R.HOTP_START_MID, R.HOTP_START_BOTTOM_MID): HasSwitch(Switch.HOTP_GHOSTS, otherwise=True), - (R.HOTP_START_MID, R.HOTP_LOWER_VOID_CONNECTION): HardLogic(HasAny(Character.ALGUS, ShopUpgrade.BRAM_WHIPLASH)), - (R.HOTP_LOWER_VOID_CONNECTION, R.HOTP_LOWER_VOID): Has(KeyItem.CLAW), + (R.HOTP_START_MID, R.HOTP_LOWER_VOID_CONNECTION): HardLogic(has_algus | has_bram_whiplash), + (R.HOTP_LOWER_VOID_CONNECTION, R.HOTP_LOWER_VOID): has_claw, (R.HOTP_LOWER_VOID, R.HOTP_UPPER_VOID): has_void, (R.HOTP_START_LEFT, R.HOTP_ELEVATOR): HasSwitch(Switch.HOTP_LEFT_BACKTRACK), - (R.HOTP_START_LEFT, R.HOTP_START_MID): Or( - HasSwitch(Switch.HOTP_LEFT_3), - And(Has(KeyItem.STAR), HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True)), - ), - (R.HOTP_START_BOTTOM, R.MECH_BRAM_TUNNEL): Has(KeyItem.STAR), - (R.HOTP_START_BOTTOM, R.HOTP_START): Or( - Has(KeyItem.STAR), - And(HasWhite(WhiteDoor.HOTP_START), Has(Eye.BLUE)), + (R.HOTP_START_LEFT, R.HOTP_START_MID): ( + HasSwitch(Switch.HOTP_LEFT_3) | (has_star & HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True)) ), - (R.HOTP_START_BOTTOM, R.HOTP_START_BOTTOM_MID): HasAll(KeyItem.BLOCK, KeyItem.BELL, KeyItem.STAR), - (R.HOTP_START_BOTTOM, R.HOTP_LOWER): Or( - HasSwitch(Switch.HOTP_BELOW_START), - CanReachRegion(R.HOTP_START_BOTTOM_MID, options=switch_off), + (R.HOTP_START_BOTTOM, R.MECH_BRAM_TUNNEL): has_star, + (R.HOTP_START_BOTTOM, R.HOTP_START): has_star | (HasWhite(WhiteDoor.HOTP_START) & Has(Eye.BLUE)), + (R.HOTP_START_BOTTOM, R.HOTP_START_BOTTOM_MID): has_block & has_star & Has(KeyItem.BELL), + (R.HOTP_START_BOTTOM, R.HOTP_LOWER): ( + HasSwitch(Switch.HOTP_BELOW_START) | CanReachRegion(R.HOTP_START_BOTTOM_MID, options=switch_off) ), (R.HOTP_START_BOTTOM_MID, R.HOTP_START_MID): HasSwitch(Switch.HOTP_GHOSTS), - (R.HOTP_START_BOTTOM_MID, R.HOTP_START_BOTTOM): Has(KeyItem.STAR), + (R.HOTP_START_BOTTOM_MID, R.HOTP_START_BOTTOM): has_star, (R.HOTP_LOWER, R.HOTP_START_BOTTOM): HasSwitch(Switch.HOTP_BELOW_START), (R.HOTP_LOWER, R.HOTP_EPIMETHEUS): HasBlue(BlueDoor.HOTP_STATUE, otherwise=True), - (R.HOTP_LOWER, R.HOTP_TP_TUTORIAL): Or( - HasSwitch(Crystal.HOTP_LOWER), - HasSwitch(Switch.HOTP_LOWER_SHORTCUT), - otherwise_crystal, + (R.HOTP_LOWER, R.HOTP_TP_TUTORIAL): ( + HasSwitch(Crystal.HOTP_LOWER) | HasSwitch(Switch.HOTP_LOWER_SHORTCUT) | otherwise_crystal ), - (R.HOTP_LOWER, R.HOTP_MECH_VOID_CONNECTION): Or( - HasSwitch(Crystal.HOTP_BOTTOM), - HardLogic(Has(ShopUpgrade.KYULI_RAY), options=switch_off), - ), - (R.HOTP_EPIMETHEUS, R.MECH_AFTER_BK): Has(KeyItem.CLAW), - (R.HOTP_MECH_VOID_CONNECTION, R.HOTP_AMULET_CONNECTION): Or( - HasSwitch(Crystal.HOTP_ROCK_ACCESS), - otherwise_crystal, + (R.HOTP_LOWER, R.HOTP_MECH_VOID_CONNECTION): ( + HasSwitch(Crystal.HOTP_BOTTOM) | HardLogic(has_kyuli_ray, options=switch_off) ), + (R.HOTP_EPIMETHEUS, R.MECH_AFTER_BK): has_claw, + (R.HOTP_MECH_VOID_CONNECTION, R.HOTP_AMULET_CONNECTION): HasSwitch(Crystal.HOTP_ROCK_ACCESS) | otherwise_crystal, (R.HOTP_MECH_VOID_CONNECTION, R.MECH_LOWER_VOID): Has(Eye.BLUE), - (R.HOTP_MECH_VOID_CONNECTION, R.HOTP_LOWER): Or(HasSwitch(Crystal.HOTP_BOTTOM), otherwise_crystal), - (R.HOTP_AMULET_CONNECTION, R.HOTP_AMULET): HasAll(KeyItem.CLAW, Eye.RED, Eye.BLUE), + (R.HOTP_MECH_VOID_CONNECTION, R.HOTP_LOWER): HasSwitch(Crystal.HOTP_BOTTOM) | otherwise_crystal, + (R.HOTP_AMULET_CONNECTION, R.HOTP_AMULET): has_claw & HasAll(Eye.RED, Eye.BLUE), (R.HOTP_AMULET_CONNECTION, R.GT_BUTT): HasSwitch(Switch.HOTP_ROCK, otherwise=True), - (R.HOTP_AMULET_CONNECTION, R.HOTP_MECH_VOID_CONNECTION): Or( - HasSwitch(Crystal.HOTP_ROCK_ACCESS), - otherwise_crystal, - ), - (R.HOTP_BELL_CAMPFIRE, R.HOTP_LOWER_ARIAS): And(Has(Character.ARIAS), Or(Has(KeyItem.BELL), can_uppies)), - (R.HOTP_BELL_CAMPFIRE, R.HOTP_RED_KEY): HasAll(Eye.GREEN, KeyItem.CLOAK), + (R.HOTP_AMULET_CONNECTION, R.HOTP_MECH_VOID_CONNECTION): HasSwitch(Crystal.HOTP_ROCK_ACCESS) | otherwise_crystal, + (R.HOTP_TP_TUTORIAL, R.HOTP_BELL_CAMPFIRE): HasSwitch(Switch.HOTP_SKULL_PUZZLE, otherwise=True), + (R.HOTP_BELL_CAMPFIRE, R.HOTP_TP_TUTORIAL): HasSwitch(Switch.HOTP_SKULL_PUZZLE), + (R.HOTP_BELL_CAMPFIRE, R.HOTP_LOWER_ARIAS): has_arias & (Has(KeyItem.BELL) | can_uppies), + (R.HOTP_BELL_CAMPFIRE, R.HOTP_RED_KEY): Has(Eye.GREEN) & has_cloak, (R.HOTP_BELL_CAMPFIRE, R.HOTP_CATH_CONNECTION): Has(Eye.GREEN), - (R.HOTP_BELL_CAMPFIRE, R.HOTP_BELL): And( - HasSwitch(Switch.HOTP_BELL_ACCESS, otherwise=True), - Or( - HasSwitch(Crystal.HOTP_BELL_ACCESS), - otherwise_crystal, - And(HasAll(KeyItem.BELL, KeyItem.BLOCK), Or(Has(Character.KYULI), can_uppies)), - HardLogic(Has(KeyItem.CLAW)), - ), - ), - (R.HOTP_CATH_CONNECTION, R.HOTP_CATH_VOID): And( - Has(KeyItem.CLAW), - Or(HasRed(RedDoor.CATH), CanReachRegion(R.HOTP_RED_KEY, options=red_off)), - ), - (R.HOTP_CATH_VOID, R.HOTP_CATH_CONNECTION): Or( - HasRed(RedDoor.CATH), - CanReachRegion(R.HOTP_RED_KEY, options=red_off), + (R.HOTP_BELL_CAMPFIRE, R.HOTP_BELL): ( + HasSwitch(Switch.HOTP_BELL_ACCESS, otherwise=True) + & ( + HasSwitch(Crystal.HOTP_BELL_ACCESS) + | otherwise_crystal + | (has_block & Has(KeyItem.BELL) & (has_kyuli | can_uppies)) + | HardLogic(has_claw) + ) + ), + (R.HOTP_CATH_CONNECTION, R.HOTP_CATH_VOID): ( + has_claw & (HasRed(RedDoor.CATH) | CanReachRegion(R.HOTP_RED_KEY, options=red_off)) + ), + (R.HOTP_CATH_VOID, R.HOTP_CATH_CONNECTION): ( + HasRed(RedDoor.CATH) | CanReachRegion(R.HOTP_RED_KEY, options=red_off) ), (R.HOTP_CATH_VOID, R.CATH_START): has_void, - (R.HOTP_LOWER_ARIAS, R.HOTP_BELL_CAMPFIRE): Has(Character.ARIAS), - (R.HOTP_LOWER_ARIAS, R.HOTP_GHOST_BLOOD): Or( - HasSwitch(Switch.HOTP_TELEPORTS, otherwise=True), - And(HasAll(KeyItem.BLOCK, KeyItem.BELL), Or(Has(Character.KYULI), can_uppies)), + (R.HOTP_LOWER_ARIAS, R.HOTP_BELL_CAMPFIRE): has_arias, + (R.HOTP_LOWER_ARIAS, R.HOTP_GHOST_BLOOD): ( + HasSwitch(Switch.HOTP_TELEPORTS, otherwise=True) | (has_block & Has(KeyItem.BELL) & (has_kyuli | can_uppies)) ), (R.HOTP_GHOST_BLOOD, R.HOTP_EYEBALL): HasSwitch(Switch.HOTP_GHOST_BLOOD, otherwise=True), (R.HOTP_GHOST_BLOOD, R.HOTP_WORM_SHORTCUT): HasSwitch(Switch.HOTP_EYEBALL_SHORTCUT), (R.HOTP_WORM_SHORTCUT, R.HOTP_GHOST_BLOOD): HasSwitch(Switch.HOTP_EYEBALL_SHORTCUT, otherwise=True), (R.HOTP_WORM_SHORTCUT, R.HOTP_ELEVATOR): HasSwitch(Switch.HOTP_WORM_PILLAR), (R.HOTP_ELEVATOR, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), - (R.HOTP_ELEVATOR, R.HOTP_OLD_MAN): And( - Has(KeyItem.CLOAK), - Or(HasSwitch(Face.HOTP_OLD_MAN), otherwise_bow), - ), + (R.HOTP_ELEVATOR, R.HOTP_OLD_MAN): has_cloak & (HasSwitch(Face.HOTP_OLD_MAN) | otherwise_bow), (R.HOTP_ELEVATOR, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), - (R.HOTP_ELEVATOR, R.HOTP_TOP_LEFT): Has(KeyItem.CLAW), + (R.HOTP_ELEVATOR, R.HOTP_TOP_LEFT): has_claw, (R.HOTP_ELEVATOR, R.CATA_BOSS): HasElevator(Elevator.CATA_2), (R.HOTP_ELEVATOR, R.TR_START): HasElevator(Elevator.TR), (R.HOTP_ELEVATOR, R.HOTP_START_LEFT): HasSwitch(Switch.HOTP_LEFT_BACKTRACK, otherwise=True), (R.HOTP_ELEVATOR, R.HOTP_WORM_SHORTCUT): HasSwitch(Switch.HOTP_WORM_PILLAR, otherwise=True), (R.HOTP_ELEVATOR, R.HOTP_SPIKE_TP_SECRET): Has(KeyItem.CHALICE), - (R.HOTP_ELEVATOR, R.HOTP_CLAW_LEFT): Or( - And(HasSwitch(Switch.HOTP_TO_CLAW_2, otherwise=True), can_extra_height), - And( - Has(KeyItem.BELL), - Or(HasAll(KeyItem.CLAW, KeyItem.CLOAK), HasAll(Character.KYULI, KeyItem.BLOCK)), - ), + (R.HOTP_ELEVATOR, R.HOTP_CLAW_LEFT): ( + (HasSwitch(Switch.HOTP_TO_CLAW_2, otherwise=True) & can_extra_height) + | (Has(KeyItem.BELL) & ((has_claw & has_cloak) | (has_kyuli & has_block))) ), (R.HOTP_ELEVATOR, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.HOTP_ELEVATOR, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), @@ -588,64 +490,44 @@ (R.HOTP_ELEVATOR, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.HOTP_CLAW_LEFT, R.HOTP_ELEVATOR): can_extra_height, (R.HOTP_CLAW_LEFT, R.HOTP_TOP_LEFT): HasWhite(WhiteDoor.HOTP_CLAW, otherwise=True), - (R.HOTP_CLAW_LEFT, R.HOTP_CLAW): Has(KeyItem.STAR), - (R.HOTP_TOP_LEFT, R.HOTP_ABOVE_OLD_MAN): And( - Has(Eye.GREEN), - Or( - HasSwitch(Switch.HOTP_TO_ABOVE_OLD_MAN, otherwise=True), - And(HasAll(KeyItem.BLOCK, KeyItem.BELL), can_uppies), - ), - ), - (R.HOTP_CLAW_CAMPFIRE, R.HOTP_CLAW): And( - HasSwitch(Switch.HOTP_CLAW_ACCESS, otherwise=True), - Or(Has(Character.KYULI), can_block_in_wall), - ), - (R.HOTP_CLAW_CAMPFIRE, R.HOTP_HEART): Or(HasSwitch(Crystal.HOTP_AFTER_CLAW), otherwise_crystal), - (R.HOTP_CLAW, R.HOTP_CLAW_CAMPFIRE): And(Has(KeyItem.CLAW), HasSwitch(Switch.HOTP_CLAW_ACCESS)), - (R.HOTP_CLAW, R.HOTP_CLAW_LEFT): Has(KeyItem.STAR), - (R.HOTP_HEART, R.HOTP_CLAW_CAMPFIRE): Or( - HasSwitch(Crystal.HOTP_AFTER_CLAW), - HardLogic( - Or( - HasAll(KeyItem.CLOAK, KeyItem.BANISH, ShopUpgrade.ALGUS_ARCANIST), - HasAll(Character.ALGUS, KeyItem.ICARUS), - Has(ShopUpgrade.KYULI_RAY), - ), + (R.HOTP_CLAW_LEFT, R.HOTP_CLAW): has_star, + (R.HOTP_TOP_LEFT, R.HOTP_ABOVE_OLD_MAN): ( + Has(Eye.GREEN) + & (HasSwitch(Switch.HOTP_TO_ABOVE_OLD_MAN, otherwise=True) | (has_block & Has(KeyItem.BELL) & can_uppies)) + ), + (R.HOTP_CLAW_CAMPFIRE, R.HOTP_CLAW): ( + HasSwitch(Switch.HOTP_CLAW_ACCESS, otherwise=True) & (has_kyuli | can_block_in_wall) + ), + (R.HOTP_CLAW_CAMPFIRE, R.HOTP_HEART): HasSwitch(Crystal.HOTP_AFTER_CLAW) | otherwise_crystal, + (R.HOTP_CLAW, R.HOTP_CLAW_CAMPFIRE): has_claw & HasSwitch(Switch.HOTP_CLAW_ACCESS), + (R.HOTP_CLAW, R.HOTP_CLAW_LEFT): has_star, + (R.HOTP_HEART, R.HOTP_CLAW_CAMPFIRE): ( + HasSwitch(Crystal.HOTP_AFTER_CLAW) + | HardLogic( + ((has_cloak & has_banish & has_algus_arcanist) | (has_algus & Has(KeyItem.ICARUS)) | has_kyuli_ray), options=switch_off, - ), + ) ), - (R.HOTP_HEART, R.HOTP_UPPER_ARIAS): Has(Character.ARIAS), - (R.HOTP_HEART, R.HOTP_BOSS_CAMPFIRE): And( - Has(KeyItem.CLAW), - Or(Has(KeyItem.ICARUS), HasAll(KeyItem.BLOCK, KeyItem.BELL), HasSwitch(Crystal.HOTP_HEART)), + (R.HOTP_HEART, R.HOTP_UPPER_ARIAS): has_arias, + (R.HOTP_HEART, R.HOTP_BOSS_CAMPFIRE): ( + has_claw & (Has(KeyItem.ICARUS) | (has_block & Has(KeyItem.BELL)) | HasSwitch(Crystal.HOTP_HEART)) ), - (R.HOTP_UPPER_ARIAS, R.HOTP_BOSS_CAMPFIRE): Has(KeyItem.CLAW), - (R.HOTP_BOSS_CAMPFIRE, R.MECH_TRIPLE_SWITCHES): And( - HasAll(Eye.GREEN, KeyItem.CLOAK), - HasSwitch(Switch.HOTP_TP_PUZZLE, Switch.MECH_ARIAS_CYCLOPS), + (R.HOTP_UPPER_ARIAS, R.HOTP_BOSS_CAMPFIRE): has_claw, + (R.HOTP_BOSS_CAMPFIRE, R.MECH_TRIPLE_SWITCHES): ( + Has(Eye.GREEN) & has_cloak & HasSwitch(Switch.HOTP_TP_PUZZLE, Switch.MECH_ARIAS_CYCLOPS) ), - (R.HOTP_BOSS_CAMPFIRE, R.HOTP_MAIDEN): And( - HasBlue(BlueDoor.HOTP_MAIDEN, otherwise=True), - Or(Has(KeyItem.SWORD), HasAll(Character.KYULI, KeyItem.BLOCK, KeyItem.BELL)), + (R.HOTP_BOSS_CAMPFIRE, R.HOTP_MAIDEN): ( + HasBlue(BlueDoor.HOTP_MAIDEN, otherwise=True) & (has_sword | (has_kyuli & has_block & Has(KeyItem.BELL))) ), (R.HOTP_BOSS_CAMPFIRE, R.HOTP_TP_PUZZLE): Has(Eye.GREEN), - (R.HOTP_BOSS_CAMPFIRE, R.HOTP_BOSS): Or( - HasWhite(WhiteDoor.HOTP_BOSS), - Has(Character.ARIAS, options=white_off), - ), - (R.HOTP_TP_PUZZLE, R.HOTP_TP_FALL_TOP): Or( - Has(KeyItem.STAR), - HasSwitch(Switch.HOTP_TP_PUZZLE, otherwise=True), - ), - (R.HOTP_TP_FALL_TOP, R.HOTP_FALL_BOTTOM): Has(KeyItem.CLOAK), - (R.HOTP_TP_FALL_TOP, R.HOTP_TP_PUZZLE): Or(Has(KeyItem.STAR), HasSwitch(Switch.HOTP_TP_PUZZLE)), - (R.HOTP_TP_FALL_TOP, R.HOTP_GAUNTLET_CONNECTION): Has(KeyItem.CLAW), - (R.HOTP_TP_FALL_TOP, R.HOTP_BOSS_CAMPFIRE): Or( - Has(Character.KYULI), - And(Has(KeyItem.BLOCK), can_combo_height), - ), - (R.HOTP_GAUNTLET_CONNECTION, R.HOTP_GAUNTLET): And(HasAll(KeyItem.CLAW, KeyItem.BELL), can_kill_ghosts), - (R.HOTP_FALL_BOTTOM, R.HOTP_TP_FALL_TOP): Has(KeyItem.CLAW), + (R.HOTP_BOSS_CAMPFIRE, R.HOTP_BOSS): HasWhite(WhiteDoor.HOTP_BOSS) | (has_arias & white_off), + (R.HOTP_TP_PUZZLE, R.HOTP_TP_FALL_TOP): has_star | HasSwitch(Switch.HOTP_TP_PUZZLE, otherwise=True), + (R.HOTP_TP_FALL_TOP, R.HOTP_FALL_BOTTOM): has_cloak, + (R.HOTP_TP_FALL_TOP, R.HOTP_TP_PUZZLE): has_star | HasSwitch(Switch.HOTP_TP_PUZZLE), + (R.HOTP_TP_FALL_TOP, R.HOTP_GAUNTLET_CONNECTION): has_claw, + (R.HOTP_TP_FALL_TOP, R.HOTP_BOSS_CAMPFIRE): has_kyuli | (has_block & can_combo_height), + (R.HOTP_GAUNTLET_CONNECTION, R.HOTP_GAUNTLET): has_claw & Has(KeyItem.BELL) & can_kill_ghosts, + (R.HOTP_FALL_BOTTOM, R.HOTP_TP_FALL_TOP): has_claw, (R.HOTP_FALL_BOTTOM, R.HOTP_UPPER_VOID): Has(Eye.GREEN), (R.HOTP_UPPER_VOID, R.HOTP_FALL_BOTTOM): Has(Eye.GREEN), (R.HOTP_UPPER_VOID, R.HOTP_LOWER_VOID): has_void, @@ -659,109 +541,80 @@ (R.HOTP_BOSS, R.GT_BOSS): HasElevator(Elevator.GT_2), (R.HOTP_BOSS, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.HOTP_BOSS, R.MECH_BOSS): HasElevator(Elevator.MECH_2), - (R.ROA_START, R.ROA_WORMS): Or( - HasSwitch(Crystal.ROA_1ST_ROOM), + (R.ROA_START, R.ROA_WORMS): ( # this should be more complicated - And(Has(KeyItem.BELL), can_crystal, options=switch_off), + HasSwitch(Crystal.ROA_1ST_ROOM) | Filtered(Has(KeyItem.BELL) & can_crystal, options=switch_off) ), - (R.ROA_WORMS, R.ROA_START): Or( - HasSwitch(Switch.ROA_WORMS, otherwise=True), - HasSwitch(Crystal.ROA_1ST_ROOM), - otherwise_crystal, + (R.ROA_WORMS, R.ROA_START): ( + HasSwitch(Switch.ROA_WORMS, otherwise=True) | HasSwitch(Crystal.ROA_1ST_ROOM) | otherwise_crystal ), - (R.ROA_WORMS, R.ROA_WORMS_CONNECTION): Or( - HasWhite(WhiteDoor.ROA_WORMS), - HasSwitch(Switch.ROA_WORMS, otherwise=True, options=white_off), + (R.ROA_WORMS, R.ROA_WORMS_CONNECTION): ( + HasWhite(WhiteDoor.ROA_WORMS) | HasSwitch(Switch.ROA_WORMS, otherwise=True, options=white_off) ), - (R.ROA_WORMS, R.ROA_LOWER_VOID_CONNECTION): Has(KeyItem.CLAW), + (R.ROA_WORMS, R.ROA_LOWER_VOID_CONNECTION): has_claw, (R.ROA_HEARTS, R.ROA_BOTTOM_ASCEND): HasSwitch(Switch.ROA_1ST_SHORTCUT), (R.ROA_WORMS_CONNECTION, R.ROA_WORMS): HasWhite(WhiteDoor.ROA_WORMS), - (R.ROA_WORMS_CONNECTION, R.ROA_HEARTS): Or( - HasSwitch(Switch.ROA_AFTER_WORMS, otherwise=True), - Has(KeyItem.STAR), - ), - (R.ROA_HEARTS, R.ROA_WORMS_CONNECTION): Or( - HasSwitch(Switch.ROA_AFTER_WORMS), - And(HasAll(KeyItem.STAR, KeyItem.BELL), can_extra_height), + (R.ROA_WORMS_CONNECTION, R.ROA_HEARTS): HasSwitch(Switch.ROA_AFTER_WORMS, otherwise=True) | has_star, + (R.ROA_HEARTS, R.ROA_WORMS_CONNECTION): ( + HasSwitch(Switch.ROA_AFTER_WORMS) | (has_star & Has(KeyItem.BELL) & can_extra_height) ), - (R.ROA_SPIKE_CLIMB, R.ROA_BOTTOM_ASCEND): Has(KeyItem.CLAW), + (R.ROA_SPIKE_CLIMB, R.ROA_BOTTOM_ASCEND): has_claw, (R.ROA_BOTTOM_ASCEND, R.ROA_TOP_ASCENT): HasWhite(WhiteDoor.ROA_ASCEND, otherwise=True), - (R.ROA_BOTTOM_ASCEND, R.ROA_TRIPLE_REAPER): Or( - HasSwitch(Switch.ROA_ASCEND, otherwise=True), - HasAll(Character.KYULI, KeyItem.BLOCK, KeyItem.BELL), + (R.ROA_BOTTOM_ASCEND, R.ROA_TRIPLE_REAPER): ( + HasSwitch(Switch.ROA_ASCEND, otherwise=True) | (has_kyuli & has_block & Has(KeyItem.BELL)) ), - (R.ROA_TRIPLE_REAPER, R.ROA_ARENA): Or(HasSwitch(Crystal.ROA_3_REAPERS), otherwise_crystal), - (R.ROA_ARENA, R.ROA_FLAMES_CONNECTION): Has(KeyItem.CLAW), + (R.ROA_TRIPLE_REAPER, R.ROA_ARENA): HasSwitch(Crystal.ROA_3_REAPERS) | otherwise_crystal, + (R.ROA_ARENA, R.ROA_FLAMES_CONNECTION): has_claw, (R.ROA_ARENA, R.ROA_TRIPLE_REAPER): HasSwitch(Crystal.ROA_3_REAPERS), - (R.ROA_ARENA, R.ROA_LOWER_VOID_CONNECTION): Has(Character.KYULI), + (R.ROA_ARENA, R.ROA_LOWER_VOID_CONNECTION): has_kyuli, (R.ROA_LOWER_VOID_CONNECTION, R.ROA_LOWER_VOID): HasSwitch(Switch.ROA_LOWER_VOID), - (R.ROA_LOWER_VOID_CONNECTION, R.ROA_ARIAS_BABY_GORGON_CONNECTION): Or( - Has(Character.KYULI), - can_uppies, - can_block_in_wall, - ), + (R.ROA_LOWER_VOID_CONNECTION, R.ROA_ARIAS_BABY_GORGON_CONNECTION): has_kyuli | can_uppies | can_block_in_wall, (R.ROA_LOWER_VOID, R.ROA_UPPER_VOID): has_void, (R.ROA_LOWER_VOID, R.ROA_LOWER_VOID_CONNECTION): HasSwitch(Switch.ROA_LOWER_VOID, otherwise=True), - (R.ROA_ARIAS_BABY_GORGON_CONNECTION, R.ROA_ARIAS_BABY_GORGON): And( - Has(Character.ARIAS), - Or(HardLogic(True_()), Has(KeyItem.BELL, options=easy)), - Or(HasSwitch(Crystal.ROA_BABY_GORGON), otherwise_crystal), - ), - (R.ROA_ARIAS_BABY_GORGON_CONNECTION, R.ROA_FLAMES_CONNECTION): HasAll(KeyItem.STAR, KeyItem.BELL), - (R.ROA_ARIAS_BABY_GORGON, R.ROA_FLAMES): And( - HasSwitch(Switch.ROA_BABY_GORGON), - HasAll(KeyItem.BLOCK, Character.KYULI, KeyItem.BELL), - ), - (R.ROA_ARIAS_BABY_GORGON, R.ROA_ARIAS_BABY_GORGON_CONNECTION): And( - Has(Character.ARIAS), - HasSwitch(Crystal.ROA_BABY_GORGON), - ), - (R.ROA_FLAMES_CONNECTION, R.ROA_WORM_CLIMB): And( - HasBlue(BlueDoor.ROA_FLAMES, otherwise=True), - Has(KeyItem.CLAW), - ), - (R.ROA_FLAMES_CONNECTION, R.ROA_LEFT_ASCENT): And( - Or(HasSwitch(Crystal.ROA_LEFT_ASCEND), And(can_crystal, Has(KeyItem.BELL), options=switch_off)), - can_extra_height, - ), - (R.ROA_FLAMES_CONNECTION, R.ROA_ARIAS_BABY_GORGON_CONNECTION): HasAll(KeyItem.STAR), - (R.ROA_FLAMES_CONNECTION, R.ROA_ARIAS_BABY_GORGON): HardLogic( - HasAny(ShopUpgrade.BRAM_AXE, ShopUpgrade.KYULI_RAY), - ), - (R.ROA_FLAMES_CONNECTION, R.ROA_FLAMES): And(HasAll(KeyItem.GAUNTLET, KeyItem.BELL), can_extra_height), - (R.ROA_FLAMES_CONNECTION, R.ROA_LEFT_ASCENT_CRYSTAL): And( - HasAll(KeyItem.BELL, Character.KYULI), - can_crystal, - ), + (R.ROA_ARIAS_BABY_GORGON_CONNECTION, R.ROA_ARIAS_BABY_GORGON): ( + has_arias + & (Has(KeyItem.BELL, options=easy) | HardLogic(True_())) + & (HasSwitch(Crystal.ROA_BABY_GORGON) | otherwise_crystal) + ), + (R.ROA_ARIAS_BABY_GORGON_CONNECTION, R.ROA_FLAMES_CONNECTION): has_star & Has(KeyItem.BELL), + (R.ROA_ARIAS_BABY_GORGON, R.ROA_FLAMES): ( + HasSwitch(Switch.ROA_BABY_GORGON) & has_block & has_kyuli & Has(KeyItem.BELL) + ), + (R.ROA_ARIAS_BABY_GORGON, R.ROA_ARIAS_BABY_GORGON_CONNECTION): has_arias & HasSwitch(Crystal.ROA_BABY_GORGON), + (R.ROA_FLAMES_CONNECTION, R.ROA_WORM_CLIMB): HasBlue(BlueDoor.ROA_FLAMES, otherwise=True) & has_claw, + (R.ROA_FLAMES_CONNECTION, R.ROA_LEFT_ASCENT): ( + (HasSwitch(Crystal.ROA_LEFT_ASCEND) | Filtered(can_crystal & Has(KeyItem.BELL), options=switch_off)) + & can_extra_height + ), + (R.ROA_FLAMES_CONNECTION, R.ROA_ARIAS_BABY_GORGON_CONNECTION): has_star, + (R.ROA_FLAMES_CONNECTION, R.ROA_ARIAS_BABY_GORGON): HardLogic(has_bram_axe | has_kyuli_ray), + (R.ROA_FLAMES_CONNECTION, R.ROA_FLAMES): has_gauntlet & Has(KeyItem.BELL) & can_extra_height, + (R.ROA_FLAMES_CONNECTION, R.ROA_LEFT_ASCENT_CRYSTAL): Has(KeyItem.BELL) & has_kyuli & can_crystal, (R.ROA_FLAMES, R.ROA_ARIAS_BABY_GORGON): HasSwitch(Switch.ROA_BABY_GORGON, otherwise=True), - (R.ROA_WORM_CLIMB, R.ROA_RIGHT_BRANCH): Has(KeyItem.CLAW), - (R.ROA_RIGHT_BRANCH, R.ROA_MIDDLE): Has(KeyItem.STAR), - (R.ROA_LEFT_ASCENT, R.ROA_FLAMES_CONNECTION): And( - Or(HasSwitch(Crystal.ROA_LEFT_ASCEND), otherwise_crystal), + (R.ROA_WORM_CLIMB, R.ROA_RIGHT_BRANCH): has_claw, + (R.ROA_RIGHT_BRANCH, R.ROA_MIDDLE): has_star, + (R.ROA_LEFT_ASCENT, R.ROA_FLAMES_CONNECTION): ( # this is overly restrictive, but whatever - HasAll(Character.KYULI, KeyItem.BELL), + (HasSwitch(Crystal.ROA_LEFT_ASCEND) | otherwise_crystal) & (has_kyuli & Has(KeyItem.BELL)) ), (R.ROA_LEFT_ASCENT, R.ROA_TOP_ASCENT): HasSwitch(Switch.ROA_ASCEND_SHORTCUT), - (R.ROA_LEFT_ASCENT, R.ROA_LEFT_ASCENT_CRYSTAL): Has(Character.ALGUS), + (R.ROA_LEFT_ASCENT, R.ROA_LEFT_ASCENT_CRYSTAL): has_algus, (R.ROA_TOP_ASCENT, R.ROA_TRIPLE_SWITCH): can_extra_height, (R.ROA_TOP_ASCENT, R.ROA_LEFT_ASCENT): HasSwitch(Switch.ROA_ASCEND_SHORTCUT), - (R.ROA_TOP_ASCENT, R.ROA_MIDDLE): And(can_extra_height, HasSwitch(Switch.ROA_ASCEND_SHORTCUT)), - (R.ROA_TRIPLE_SWITCH, R.ROA_MIDDLE): And( - Or(HasSwitch(Switch.ROA_TRIPLE_1, Switch.ROA_TRIPLE_3), otherwise_crystal), - HasAll(KeyItem.CLAW, KeyItem.BELL), + (R.ROA_TOP_ASCENT, R.ROA_MIDDLE): can_extra_height & HasSwitch(Switch.ROA_ASCEND_SHORTCUT), + (R.ROA_TRIPLE_SWITCH, R.ROA_MIDDLE): ( + (HasSwitch(Switch.ROA_TRIPLE_1, Switch.ROA_TRIPLE_3) | otherwise_crystal) & has_claw & Has(KeyItem.BELL) ), (R.ROA_MIDDLE, R.ROA_LEFT_SWITCH): can_extra_height, - (R.ROA_MIDDLE, R.ROA_RIGHT_BRANCH): Has(KeyItem.STAR), - (R.ROA_MIDDLE, R.ROA_RIGHT_SWITCH_1): Or(Has(Character.KYULI), HasSwitch(Switch.ROA_RIGHT_PATH)), - (R.ROA_MIDDLE, R.ROA_MIDDLE_LADDER): Or( + (R.ROA_MIDDLE, R.ROA_RIGHT_BRANCH): has_star, + (R.ROA_MIDDLE, R.ROA_RIGHT_SWITCH_1): has_kyuli | HasSwitch(Switch.ROA_RIGHT_PATH), + (R.ROA_MIDDLE, R.ROA_MIDDLE_LADDER): ( # this could allow more - HasSwitch(Crystal.ROA_LADDER_L, Crystal.ROA_LADDER_R), - And( - can_crystal, - CanReachRegion(R.ROA_LEFT_SWITCH), - CanReachRegion(R.ROA_RIGHT_SWITCH_2), + HasSwitch(Crystal.ROA_LADDER_L, Crystal.ROA_LADDER_R) + | Filtered( + can_crystal & CanReachRegion(R.ROA_LEFT_SWITCH) & CanReachRegion(R.ROA_RIGHT_SWITCH_2), options=switch_off, - ), + ) ), (R.ROA_MIDDLE, R.ROA_TOP_ASCENT): HasSwitch(Switch.ROA_ASCEND_SHORTCUT, otherwise=True), (R.ROA_MIDDLE, R.ROA_TRIPLE_SWITCH): HasSwitch(Switch.ROA_TRIPLE_1, Switch.ROA_TRIPLE_3), @@ -772,29 +625,22 @@ Switch.ROA_SHAFT_R, otherwise=True, ), - (R.ROA_MIDDLE_LADDER, R.ROA_RIGHT_SWITCH_CANDLE): HasAny( - Character.ALGUS, - ShopUpgrade.BRAM_AXE, - ShopUpgrade.BRAM_WHIPLASH, - ), + (R.ROA_MIDDLE_LADDER, R.ROA_RIGHT_SWITCH_CANDLE): has_algus | has_bram_axe | has_bram_whiplash, (R.ROA_UPPER_VOID, R.ROA_LOWER_VOID): has_void, (R.ROA_UPPER_VOID, R.ROA_SP_CONNECTION): HasSwitch(Crystal.ROA_SHAFT, Switch.ROA_SHAFT_DOWNWARDS), - (R.ROA_UPPER_VOID, R.ROA_SPIKE_BALLS): Or(HasSwitch(Crystal.ROA_SPIKE_BALLS), otherwise_crystal), + (R.ROA_UPPER_VOID, R.ROA_SPIKE_BALLS): HasSwitch(Crystal.ROA_SPIKE_BALLS) | otherwise_crystal, (R.ROA_SPIKE_BALLS, R.ROA_SPIKE_SPINNERS): HasWhite(WhiteDoor.ROA_BALLS, otherwise=True), (R.ROA_SPIKE_SPINNERS, R.ROA_SPIDERS_1): HasWhite(WhiteDoor.ROA_SPINNERS, otherwise=True), (R.ROA_SPIKE_SPINNERS, R.ROA_SPIKE_BALLS): HasWhite(WhiteDoor.ROA_BALLS, otherwise=True), - (R.ROA_SPIDERS_1, R.ROA_RED_KEY): Or(HasSwitch(Face.ROA_SPIDERS), otherwise_bow), + (R.ROA_SPIDERS_1, R.ROA_RED_KEY): HasSwitch(Face.ROA_SPIDERS) | otherwise_bow, (R.ROA_SPIDERS_1, R.ROA_SPIDERS_2): can_extra_height, (R.ROA_SPIDERS_2, R.ROA_BLOOD_POT_HALLWAY): HasSwitch(Switch.ROA_SPIDERS, otherwise=True), - (R.ROA_SP_CONNECTION, R.SP_START): Or( - HasRed(RedDoor.SP), - And(HasAll(KeyItem.CLOAK, KeyItem.CLAW, KeyItem.BELL), CanReachRegion(R.ROA_RED_KEY), options=red_off), - ), - (R.ROA_SP_CONNECTION, R.ROA_ELEVATOR): And( - # can probably make it without claw - Has(KeyItem.CLAW), - HasSwitch(Switch.ROA_DARK_ROOM, otherwise=True), + (R.ROA_SP_CONNECTION, R.SP_START): ( + HasRed(RedDoor.SP) + | Filtered(has_cloak & has_claw & Has(KeyItem.BELL) & CanReachRegion(R.ROA_RED_KEY), options=red_off) ), + # can probably make it without claw + (R.ROA_SP_CONNECTION, R.ROA_ELEVATOR): has_claw & HasSwitch(Switch.ROA_DARK_ROOM, otherwise=True), (R.ROA_ELEVATOR, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), (R.ROA_ELEVATOR, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.ROA_ELEVATOR, R.CATA_BOSS): HasElevator(Elevator.CATA_2), @@ -802,41 +648,32 @@ (R.ROA_ELEVATOR, R.TR_START): HasElevator(Elevator.TR), (R.ROA_ELEVATOR, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.ROA_ELEVATOR, R.ROA_ICARUS): HasSwitch(Switch.ROA_ICARUS, otherwise=True), - (R.ROA_ELEVATOR, R.ROA_DARK_CONNECTION): Or( - Has(KeyItem.CLAW), - HasSwitch(Switch.ROA_ELEVATOR, otherwise=True), - ), + (R.ROA_ELEVATOR, R.ROA_DARK_CONNECTION): has_claw | HasSwitch(Switch.ROA_ELEVATOR, otherwise=True), (R.ROA_ELEVATOR, R.APEX): elevator_apex, (R.ROA_ELEVATOR, R.GT_BOSS): HasElevator(Elevator.GT_2), (R.ROA_ELEVATOR, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.ROA_ELEVATOR, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.ROA_DARK_CONNECTION, R.ROA_TOP_CENTAUR): HasSwitch(Switch.ROA_BLOOD_POT), (R.ROA_DARK_CONNECTION, R.DARK_START): can_extra_height, - (R.DARK_START, R.DARK_END): And(Has(KeyItem.CLAW), HasSwitch(Switch.DARKNESS, otherwise=True)), - (R.DARK_END, R.ROA_DARK_EXIT): Has(KeyItem.CLAW), - (R.ROA_DARK_EXIT, R.ROA_ABOVE_CENTAUR_R): And( - HasAll(Character.ARIAS, KeyItem.BELL), - Or(HardLogic(True_()), Has(Character.KYULI)), - ), - (R.ROA_DARK_EXIT, R.ROA_CRYSTAL_ABOVE_CENTAUR): HardLogic(Has(ShopUpgrade.KYULI_RAY)), - (R.ROA_TOP_CENTAUR, R.ROA_DARK_CONNECTION): Or( - HasSwitch(Switch.ROA_BLOOD_POT, otherwise=True), - HasBlue(BlueDoor.ROA_BLOOD, otherwise=True), + (R.DARK_START, R.DARK_END): has_claw & HasSwitch(Switch.DARKNESS, otherwise=True), + (R.DARK_END, R.ROA_DARK_EXIT): has_claw, + (R.ROA_DARK_EXIT, R.ROA_ABOVE_CENTAUR_R): has_arias & Has(KeyItem.BELL) & (has_kyuli | HardLogic(True_())), + (R.ROA_DARK_EXIT, R.ROA_CRYSTAL_ABOVE_CENTAUR): HardLogic(has_kyuli_ray), + (R.ROA_TOP_CENTAUR, R.ROA_DARK_CONNECTION): ( + HasSwitch(Switch.ROA_BLOOD_POT, otherwise=True) | HasBlue(BlueDoor.ROA_BLOOD, otherwise=True) ), (R.ROA_TOP_CENTAUR, R.ROA_DARK_EXIT): can_extra_height, - (R.ROA_TOP_CENTAUR, R.ROA_BOSS_CONNECTION): Or( - HasSwitch(Crystal.ROA_CENTAUR), - CanReachRegion(R.ROA_CRYSTAL_ABOVE_CENTAUR), - ), - (R.ROA_ABOVE_CENTAUR_R, R.ROA_DARK_EXIT): HasAll(Character.ARIAS, KeyItem.BELL), - (R.ROA_ABOVE_CENTAUR_R, R.ROA_ABOVE_CENTAUR_L): HasAll(KeyItem.STAR, KeyItem.BELL), - (R.ROA_ABOVE_CENTAUR_R, R.ROA_CRYSTAL_ABOVE_CENTAUR): can_crystal_wo_whiplash, - (R.ROA_ABOVE_CENTAUR_L, R.ROA_ABOVE_CENTAUR_R): HasAll(KeyItem.STAR, KeyItem.BELL), - (R.ROA_ABOVE_CENTAUR_L, R.ROA_CRYSTAL_ABOVE_CENTAUR): can_crystal_wo_block, + (R.ROA_TOP_CENTAUR, R.ROA_BOSS_CONNECTION): ( + HasSwitch(Crystal.ROA_CENTAUR) | CanReachRegion(R.ROA_CRYSTAL_ABOVE_CENTAUR, options=switch_off) + ), + (R.ROA_ABOVE_CENTAUR_R, R.ROA_DARK_EXIT): has_arias & Has(KeyItem.BELL), + (R.ROA_ABOVE_CENTAUR_R, R.ROA_ABOVE_CENTAUR_L): has_star & Has(KeyItem.BELL), + (R.ROA_ABOVE_CENTAUR_R, R.ROA_CRYSTAL_ABOVE_CENTAUR): can_crystal_no_whiplash & Has(KeyItem.BELL), + (R.ROA_ABOVE_CENTAUR_L, R.ROA_ABOVE_CENTAUR_R): has_star & Has(KeyItem.BELL), + (R.ROA_ABOVE_CENTAUR_L, R.ROA_CRYSTAL_ABOVE_CENTAUR): can_crystal_no_block, (R.ROA_BOSS_CONNECTION, R.ROA_ABOVE_CENTAUR_L): can_extra_height, - (R.ROA_BOSS_CONNECTION, R.ROA_TOP_CENTAUR): Or( - HasSwitch(Crystal.ROA_CENTAUR), - CanReachRegion(R.ROA_CRYSTAL_ABOVE_CENTAUR), + (R.ROA_BOSS_CONNECTION, R.ROA_TOP_CENTAUR): ( + HasSwitch(Crystal.ROA_CENTAUR) | CanReachRegion(R.ROA_CRYSTAL_ABOVE_CENTAUR, options=switch_off) ), (R.ROA_BOSS_CONNECTION, R.ROA_BOSS): HasSwitch(Switch.ROA_BOSS_ACCESS, otherwise=True), (R.ROA_BOSS, R.ROA_APEX_CONNECTION): Has(Eye.GREEN), @@ -847,10 +684,8 @@ (R.APEX, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.APEX, R.CATA_BOSS): HasElevator(Elevator.CATA_2), (R.APEX, R.HOTP_ELEVATOR): HasElevator(Elevator.HOTP), - (R.APEX, R.FINAL_BOSS): And( - HasAll(Eye.RED, Eye.BLUE, Eye.GREEN), - Or(HardLogic(True_()), Has(KeyItem.BELL, options=easy)), - HasGoal(), + (R.APEX, R.FINAL_BOSS): ( + HasAll(Eye.RED, Eye.BLUE, Eye.GREEN) & (Has(KeyItem.BELL, options=easy) | HardLogic(True_())) & HasGoal() ), (R.APEX, R.ROA_APEX_CONNECTION): HasSwitch(Switch.ROA_APEX_ACCESS), (R.APEX, R.TR_START): HasElevator(Elevator.TR), @@ -858,17 +693,14 @@ (R.APEX, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.APEX, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), (R.APEX, R.GT_BOSS): HasElevator(Elevator.GT_2), - (R.APEX, R.APEX_CENTAUR_ACCESS): And(HasBlue(BlueDoor.APEX, otherwise=True), Has(KeyItem.STAR)), + (R.APEX, R.APEX_CENTAUR_ACCESS): HasBlue(BlueDoor.APEX, otherwise=True) & has_star, (R.APEX, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.APEX, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.APEX_CENTAUR_ACCESS, R.APEX_CENTAUR): Has(KeyItem.ADORNED_KEY), (R.CAVES_START, R.CAVES_EPIMETHEUS): HasBlue(BlueDoor.CAVES, otherwise=True), - (R.CAVES_EPIMETHEUS, R.CAVES_UPPER): Or(Has(Character.KYULI), can_block_in_wall, can_combo_height), + (R.CAVES_EPIMETHEUS, R.CAVES_UPPER): has_kyuli | can_block_in_wall | can_combo_height, (R.CAVES_EPIMETHEUS, R.CAVES_START): HasBlue(BlueDoor.CAVES, otherwise=True), - (R.CAVES_UPPER, R.CAVES_ARENA): Or( - HasAny(KeyItem.SWORD, ShopUpgrade.KYULI_RAY), - And(Has(ShopUpgrade.ALGUS_METEOR), chalice_on_easy), - ), + (R.CAVES_UPPER, R.CAVES_ARENA): has_sword | has_kyuli_ray | (has_algus_meteor & chalice_on_easy), (R.CAVES_UPPER, R.CAVES_LOWER): HasSwitch(Switch.CAVES_SKELETONS, otherwise=True), (R.CAVES_LOWER, R.CAVES_UPPER): HasSwitch(Switch.CAVES_SKELETONS), (R.CAVES_LOWER, R.CAVES_ITEM_CHAIN): Has(Eye.RED), @@ -880,8 +712,8 @@ ), (R.CATA_START, R.CATA_CLIMBABLE_ROOT): HasSwitch(Switch.CATA_1ST_ROOM, otherwise=True), (R.CATA_START, R.CAVES_LOWER): HasSwitch(Switch.CAVES_CATA_1, Switch.CAVES_CATA_2, Switch.CAVES_CATA_3), - (R.CATA_CLIMBABLE_ROOT, R.CATA_TOP): And(Has(Eye.RED), HasWhite(WhiteDoor.CATA_TOP, otherwise=True)), - (R.CATA_TOP, R.CATA_CLIMBABLE_ROOT): And(Has(Eye.RED), HasWhite(WhiteDoor.CATA_TOP, otherwise=True)), + (R.CATA_CLIMBABLE_ROOT, R.CATA_TOP): Has(Eye.RED) & HasWhite(WhiteDoor.CATA_TOP, otherwise=True), + (R.CATA_TOP, R.CATA_CLIMBABLE_ROOT): Has(Eye.RED) & HasWhite(WhiteDoor.CATA_TOP, otherwise=True), (R.CATA_TOP, R.CATA_ELEVATOR): HasSwitch(Switch.CATA_ELEVATOR, otherwise=True), (R.CATA_TOP, R.CATA_BOW_CAMPFIRE): HasSwitch(Switch.CATA_TOP, otherwise=True), (R.CATA_ELEVATOR, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), @@ -897,31 +729,23 @@ (R.CATA_ELEVATOR, R.CATA_MULTI): HasBlue(BlueDoor.CATA_ORBS, otherwise=True), (R.CATA_ELEVATOR, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.CATA_BOW_CAMPFIRE, R.CATA_TOP): HasSwitch(Switch.CATA_TOP), - (R.CATA_BOW_CAMPFIRE, R.CATA_BOW_CONNECTION): And( - Has(Character.KYULI), - HasBlue(BlueDoor.CATA_SAVE, otherwise=True), - ), - (R.CATA_BOW_CAMPFIRE, R.CATA_EYEBALL_BONES): Or(HasSwitch(Face.CATA_AFTER_BOW), otherwise_bow), - (R.CATA_BOW_CONNECTION, R.CATA_BOW): And( - HasBlue(BlueDoor.CATA_BOW, otherwise=True), - Has(Character.KYULI), - ), + (R.CATA_BOW_CAMPFIRE, R.CATA_BOW_CONNECTION): has_kyuli & HasBlue(BlueDoor.CATA_SAVE, otherwise=True), + (R.CATA_BOW_CAMPFIRE, R.CATA_EYEBALL_BONES): HasSwitch(Face.CATA_AFTER_BOW) | otherwise_bow, + (R.CATA_BOW_CONNECTION, R.CATA_BOW): HasBlue(BlueDoor.CATA_BOW, otherwise=True) & has_kyuli, (R.CATA_BOW_CONNECTION, R.CATA_BOW_CAMPFIRE): HasBlue(BlueDoor.CATA_SAVE, otherwise=True), (R.CATA_BOW_CONNECTION, R.CATA_VERTICAL_SHORTCUT): HasSwitch(Switch.CATA_VERTICAL_SHORTCUT), - (R.CATA_VERTICAL_SHORTCUT, R.CATA_BOW_CONNECTION): And( - HasSwitch(Switch.CATA_VERTICAL_SHORTCUT, otherwise=True), - Or(HasSwitch(Switch.CATA_MID_SHORTCUT, otherwise=True), HasAll(Character.KYULI, KeyItem.ICARUS)), + (R.CATA_VERTICAL_SHORTCUT, R.CATA_BOW_CONNECTION): ( + HasSwitch(Switch.CATA_VERTICAL_SHORTCUT, otherwise=True) + & (HasSwitch(Switch.CATA_MID_SHORTCUT, otherwise=True) | (has_kyuli & Has(KeyItem.ICARUS))) ), (R.CATA_EYEBALL_BONES, R.CATA_SNAKE_MUSHROOMS): Has(Eye.RED), - (R.CATA_SNAKE_MUSHROOMS, R.CATA_DEV_ROOM_CONNECTION): HasAll(KeyItem.CLAW, KeyItem.BELL, Character.ZEEK), + (R.CATA_SNAKE_MUSHROOMS, R.CATA_DEV_ROOM_CONNECTION): has_claw & Has(KeyItem.BELL) & has_zeek, (R.CATA_SNAKE_MUSHROOMS, R.CATA_EYEBALL_BONES): Has(Eye.RED), - (R.CATA_SNAKE_MUSHROOMS, R.CATA_DOUBLE_SWITCH): And( - HasSwitch(Switch.CATA_CLAW_2, otherwise=True), - Or(Has(KeyItem.CLAW), HasAll(Character.KYULI, Character.ZEEK, KeyItem.BELL)), + (R.CATA_SNAKE_MUSHROOMS, R.CATA_DOUBLE_SWITCH): ( + HasSwitch(Switch.CATA_CLAW_2, otherwise=True) & (has_claw | (has_kyuli & has_zeek & Has(KeyItem.BELL))) ), - (R.CATA_DEV_ROOM_CONNECTION, R.CATA_DEV_ROOM): Or( - HasRed(RedDoor.DEV_ROOM), - And(HasAll(Character.ZEEK, Character.KYULI), CanReachRegion(R.GT_BOSS), options=red_off), + (R.CATA_DEV_ROOM_CONNECTION, R.CATA_DEV_ROOM): ( + HasRed(RedDoor.DEV_ROOM) | Filtered(has_zeek & has_kyuli & CanReachRegion(R.GT_BOSS), options=red_off) ), (R.CATA_DOUBLE_SWITCH, R.CATA_SNAKE_MUSHROOMS): HasSwitch(Switch.CATA_CLAW_2), (R.CATA_DOUBLE_SWITCH, R.CATA_ROOTS_CAMPFIRE): HasSwitch( @@ -930,45 +754,38 @@ otherwise=True, ), (R.CATA_ROOTS_CAMPFIRE, R.CATA_DOUBLE_SWITCH): HasSwitch(Switch.CATA_WATER_1, Switch.CATA_WATER_2), - (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_ROOTS_CAMPFIRE): Has(KeyItem.CLAW), + (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_ROOTS_CAMPFIRE): has_claw, (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_BLUE_EYE_DOOR): Has(Eye.BLUE), - (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_ABOVE_ROOTS): Has(KeyItem.CLAW), - (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_POISON_ROOTS): And( - HasBlue(BlueDoor.CATA_ROOTS, otherwise=True), - Has(Character.KYULI), - ), + (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_ABOVE_ROOTS): has_claw, + (R.CATA_BELOW_ROOTS_CAMPFIRE, R.CATA_POISON_ROOTS): HasBlue(BlueDoor.CATA_ROOTS, otherwise=True) & has_kyuli, (R.CATA_BLUE_EYE_DOOR, R.CATA_BELOW_ROOTS_CAMPFIRE): Has(Eye.BLUE), (R.CATA_BLUE_EYE_DOOR, R.CATA_FLAMES_FORK): HasWhite(WhiteDoor.CATA_BLUE, otherwise=True), - (R.CATA_FLAMES_FORK, R.CATA_VERTICAL_SHORTCUT): Or( - HasSwitch(Switch.CATA_SHORTCUT_ACCESS, Switch.CATA_AFTER_BLUE_DOOR, otherwise=True), - HardLogic(Has(KeyItem.CLAW)), + (R.CATA_FLAMES_FORK, R.CATA_VERTICAL_SHORTCUT): ( + HasSwitch(Switch.CATA_SHORTCUT_ACCESS, Switch.CATA_AFTER_BLUE_DOOR, otherwise=True) | HardLogic(has_claw) ), - (R.CATA_FLAMES_FORK, R.CATA_BLUE_EYE_DOOR): Or( - HasWhite(WhiteDoor.CATA_BLUE, otherwise=True), - HasSwitch(Switch.CATA_SHORTCUT_ACCESS, otherwise=True), + (R.CATA_FLAMES_FORK, R.CATA_BLUE_EYE_DOOR): ( + HasWhite(WhiteDoor.CATA_BLUE, otherwise=True) | HasSwitch(Switch.CATA_SHORTCUT_ACCESS, otherwise=True) ), (R.CATA_FLAMES_FORK, R.CATA_FLAMES): HasSwitch(Switch.CATA_FLAMES_2, otherwise=True), (R.CATA_FLAMES_FORK, R.CATA_CENTAUR): HasSwitch(Switch.CATA_LADDER_BLOCKS, otherwise=True), - (R.CATA_CENTAUR, R.CATA_4_FACES): Has(KeyItem.CLAW), + (R.CATA_CENTAUR, R.CATA_4_FACES): has_claw, (R.CATA_CENTAUR, R.CATA_FLAMES_FORK): HasSwitch(Switch.CATA_LADDER_BLOCKS), (R.CATA_CENTAUR, R.CATA_BOSS): HasSwitch(Face.CATA_CAMPFIRE), - (R.CATA_4_FACES, R.CATA_DOUBLE_DOOR): Or(HasSwitch(Face.CATA_X4), otherwise_bow), + (R.CATA_4_FACES, R.CATA_DOUBLE_DOOR): HasSwitch(Face.CATA_X4) | otherwise_bow, (R.CATA_DOUBLE_DOOR, R.CATA_4_FACES): HasSwitch(Face.CATA_X4), - (R.CATA_DOUBLE_DOOR, R.CATA_VOID_R): And( - Has(KeyItem.BELL), - can_kill_ghosts, - Or(HasSwitch(Face.CATA_DOUBLE_DOOR), otherwise_bow), + (R.CATA_DOUBLE_DOOR, R.CATA_VOID_R): ( + Has(KeyItem.BELL) & can_kill_ghosts & (HasSwitch(Face.CATA_DOUBLE_DOOR) | otherwise_bow) ), - (R.CATA_VOID_R, R.CATA_DOUBLE_DOOR): HardLogic(HasAll(KeyItem.BELL, ShopUpgrade.ALGUS_METEOR)), + (R.CATA_VOID_R, R.CATA_DOUBLE_DOOR): HardLogic(Has(KeyItem.BELL) & has_algus_meteor), (R.CATA_VOID_R, R.CATA_VOID_L): has_void, (R.CATA_VOID_L, R.CATA_VOID_R): has_void, - (R.CATA_VOID_L, R.CATA_BOSS): And(HasWhite(WhiteDoor.CATA_PRISON, otherwise=True), Has(Character.KYULI)), + (R.CATA_VOID_L, R.CATA_BOSS): HasWhite(WhiteDoor.CATA_PRISON, otherwise=True) & has_kyuli, (R.CATA_BOSS, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), (R.CATA_BOSS, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), (R.CATA_BOSS, R.HOTP_ELEVATOR): HasElevator(Elevator.HOTP), - (R.CATA_BOSS, R.CATA_CENTAUR): Or(HasSwitch(Face.CATA_CAMPFIRE), otherwise_bow), + (R.CATA_BOSS, R.CATA_CENTAUR): HasSwitch(Face.CATA_CAMPFIRE) | otherwise_bow, (R.CATA_BOSS, R.CATA_VOID_L): HasWhite(WhiteDoor.CATA_PRISON, otherwise=True), - (R.CATA_BOSS, R.TR_START): Or(HasElevator(Elevator.TR), HasSwitch(Switch.TR_ELEVATOR, otherwise=True)), + (R.CATA_BOSS, R.TR_START): HasElevator(Elevator.TR) | HasSwitch(Switch.TR_ELEVATOR, otherwise=True), (R.CATA_BOSS, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.CATA_BOSS, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), (R.CATA_BOSS, R.APEX): elevator_apex, @@ -977,203 +794,174 @@ (R.CATA_BOSS, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.TR_START, R.GT_ENTRANCE): HasElevator(Elevator.GT_1), (R.TR_START, R.CATA_ELEVATOR): HasElevator(Elevator.CATA_1), - (R.TR_START, R.CATA_BOSS): Or( - HasElevator(Elevator.CATA_2), - And(HasSwitch(Switch.TR_ELEVATOR), can_extra_height), - ), + (R.TR_START, R.CATA_BOSS): HasElevator(Elevator.CATA_2) | (HasSwitch(Switch.TR_ELEVATOR) & can_extra_height), (R.TR_START, R.HOTP_ELEVATOR): HasElevator(Elevator.HOTP), (R.TR_START, R.HOTP_BOSS): HasElevator(Elevator.ROA_1), (R.TR_START, R.ROA_ELEVATOR): HasElevator(Elevator.ROA_2), - (R.TR_START, R.TR_LEFT): And( - HasBlue(BlueDoor.TR, otherwise=True), - Or(HasRed(RedDoor.TR), And(Has(KeyItem.CLAW), CanReachRegion(R.CATA_BOSS), options=red_off)), - can_extra_height, + (R.TR_START, R.TR_LEFT): ( + HasBlue(BlueDoor.TR, otherwise=True) + & (HasRed(RedDoor.TR) | Filtered(has_claw & CanReachRegion(R.CATA_BOSS), options=red_off)) + & can_extra_height ), (R.TR_START, R.APEX): elevator_apex, (R.TR_START, R.GT_BOSS): HasElevator(Elevator.GT_2), (R.TR_START, R.MECH_ZEEK_CONNECTION): HasElevator(Elevator.MECH_1), (R.TR_START, R.MECH_BOSS): HasElevator(Elevator.MECH_2), (R.TR_START, R.TR_BRAM): Has(Eye.BLUE), - (R.TR_LEFT, R.TR_TOP_RIGHT): HasAll(KeyItem.STAR, KeyItem.BELL), - (R.TR_LEFT, R.TR_BOTTOM_LEFT): And(Has(KeyItem.BELL), can_kill_ghosts), + (R.TR_LEFT, R.TR_TOP_RIGHT): has_star & Has(KeyItem.BELL), + (R.TR_LEFT, R.TR_BOTTOM_LEFT): Has(KeyItem.BELL) & can_kill_ghosts, (R.TR_BOTTOM_LEFT, R.TR_BOTTOM): Has(Eye.BLUE), - (R.TR_TOP_RIGHT, R.TR_GOLD): And( - HasAll(Character.ZEEK, KeyItem.BELL), - Or(HasAny(Character.KYULI, KeyItem.BLOCK), can_uppies), - ), - (R.TR_TOP_RIGHT, R.TR_MIDDLE_RIGHT): Or( - HasSwitch(Crystal.TR_GOLD), - And(HasAll(KeyItem.BELL, KeyItem.CLAW), can_crystal, options=switch_off), + (R.TR_TOP_RIGHT, R.TR_GOLD): has_zeek & Has(KeyItem.BELL) & (has_kyuli | has_block | can_uppies), + (R.TR_TOP_RIGHT, R.TR_MIDDLE_RIGHT): ( + HasSwitch(Crystal.TR_GOLD) | Filtered(Has(KeyItem.BELL) & has_claw & can_crystal, options=switch_off) ), (R.TR_MIDDLE_RIGHT, R.TR_DARK_ARIAS): Has(Eye.GREEN), (R.TR_MIDDLE_RIGHT, R.TR_BOTTOM): HasSwitch(Switch.TR_BOTTOM, otherwise=True), (R.TR_BOTTOM, R.TR_BOTTOM_LEFT): Has(Eye.BLUE), - (R.CD_START, R.CD_2): Or(HasSwitch(Switch.CD_1, otherwise=True), HasSwitch(Crystal.CD_BACKTRACK)), - (R.CD_START, R.CD_BOSS): And(CanReachRegion(R.CD_ARIAS_ROUTE), CanReachRegion(R.CD_TOP)), + (R.CD_START, R.CD_2): HasSwitch(Switch.CD_1, otherwise=True) | HasSwitch(Crystal.CD_BACKTRACK), + (R.CD_START, R.CD_BOSS): CanReachRegion(R.CD_ARIAS_ROUTE) & CanReachRegion(R.CD_TOP), (R.CD_3, R.CD_MIDDLE): HasSwitch(Switch.CD_3, otherwise=True), (R.CD_MIDDLE, R.CD_KYULI_ROUTE): HasSwitch(Switch.CD_CAMPFIRE, otherwise=True), - (R.CD_MIDDLE, R.CD_ARIAS_ROUTE): Has(Character.ARIAS), - (R.CD_KYULI_ROUTE, R.CD_CAMPFIRE_3): Has(Character.KYULI), - (R.CD_CAMPFIRE_3, R.CD_ARENA): Or(HasSwitch(Crystal.CD_CAMPFIRE), otherwise_crystal), - (R.CD_STEPS, R.CD_TOP): Or(HasSwitch(Crystal.CD_STEPS), otherwise_crystal), - (R.CATH_START, R.CATH_START_LEFT): And( - Or( - HasSwitch(Crystal.CATH_1ST_ROOM), - And(can_crystal, CanReachRegion(R.CATH_START_TOP_LEFT), options=switch_off), - ), - Has(KeyItem.CLAW), + (R.CD_MIDDLE, R.CD_ARIAS_ROUTE): has_arias, + (R.CD_KYULI_ROUTE, R.CD_CAMPFIRE_3): has_kyuli, + (R.CD_CAMPFIRE_3, R.CD_ARENA): HasSwitch(Crystal.CD_CAMPFIRE) | otherwise_crystal, + (R.CD_STEPS, R.CD_TOP): HasSwitch(Crystal.CD_STEPS) | otherwise_crystal, + (R.CATH_START, R.CATH_START_LEFT): ( + ( + HasSwitch(Crystal.CATH_1ST_ROOM) + | Filtered(can_crystal & CanReachRegion(R.CATH_START_TOP_LEFT), options=switch_off) + ) + & has_claw ), (R.CATH_START_RIGHT, R.CATH_START_TOP_LEFT): HasSwitch(Switch.CATH_BOTTOM, otherwise=True), (R.CATH_START_TOP_LEFT, R.CATH_START_LEFT): HasSwitch(Face.CATH_L), - (R.CATH_START_LEFT, R.CATH_TP): Or(HasSwitch(Face.CATH_R), otherwise_bow), - (R.CATH_LEFT_SHAFT, R.CATH_SHAFT_ACCESS): And(HasSwitch(Crystal.CATH_SHAFT_ACCESS), Has(KeyItem.CLAW)), - (R.CATH_LEFT_SHAFT, R.CATH_UNDER_CAMPFIRE): Or(HasSwitch(Crystal.CATH_SHAFT), otherwise_crystal), - (R.CATH_UNDER_CAMPFIRE, R.CATH_CAMPFIRE_1): HasAll(Character.ZEEK, KeyItem.BELL), - (R.CATH_CAMPFIRE_1, R.CATH_SHAFT_ACCESS): Has(Character.KYULI), + (R.CATH_START_LEFT, R.CATH_TP): HasSwitch(Face.CATH_R) | otherwise_bow, + (R.CATH_LEFT_SHAFT, R.CATH_SHAFT_ACCESS): HasSwitch(Crystal.CATH_SHAFT_ACCESS) & has_claw, + (R.CATH_LEFT_SHAFT, R.CATH_UNDER_CAMPFIRE): HasSwitch(Crystal.CATH_SHAFT) | otherwise_crystal, + (R.CATH_UNDER_CAMPFIRE, R.CATH_CAMPFIRE_1): has_zeek & Has(KeyItem.BELL), + (R.CATH_CAMPFIRE_1, R.CATH_SHAFT_ACCESS): has_kyuli, (R.CATH_SHAFT_ACCESS, R.CATH_ORB_ROOM): HasSwitch(Switch.CATH_BESIDE_SHAFT, otherwise=True), - (R.CATH_ORB_ROOM, R.CATH_GOLD_BLOCK): Or( - HasSwitch(Crystal.CATH_ORBS), - And(can_crystal, Has(KeyItem.BELL), options=switch_off), + (R.CATH_ORB_ROOM, R.CATH_GOLD_BLOCK): ( + HasSwitch(Crystal.CATH_ORBS) | Filtered(can_crystal & Has(KeyItem.BELL), options=switch_off) ), - (R.CATH_RIGHT_SHAFT_CONNECTION, R.CATH_RIGHT_SHAFT): HasAll(KeyItem.BELL, Character.ZEEK, KeyItem.BOW), - (R.CATH_RIGHT_SHAFT, R.CATH_TOP): Has(KeyItem.CLAW), - (R.CATH_TOP, R.CATH_UPPER_SPIKE_PIT): Or( - HasSwitch(Crystal.CATH_SPIKE_PIT), - otherwise_crystal, - HardLogic(HasAll(KeyItem.CLOAK, KeyItem.BLOCK, KeyItem.BELL)), + (R.CATH_RIGHT_SHAFT_CONNECTION, R.CATH_RIGHT_SHAFT): Has(KeyItem.BELL) & has_zeek & has_bow, + (R.CATH_RIGHT_SHAFT, R.CATH_TOP): has_claw, + (R.CATH_TOP, R.CATH_UPPER_SPIKE_PIT): ( + HasSwitch(Crystal.CATH_SPIKE_PIT) | otherwise_crystal | HardLogic(has_cloak & has_block & Has(KeyItem.BELL)) ), (R.CATH_TOP, R.CATH_CAMPFIRE_2): HasSwitch(Switch.CATH_TOP_CAMPFIRE, otherwise=True), - (R.SP_START, R.SP_STAR_END): HasAll(KeyItem.BLOCK, KeyItem.BELL, KeyItem.CLAW), - (R.SP_START, R.SP_CAMPFIRE_1): Or(HasSwitch(Crystal.SP_BLOCKS), otherwise_crystal), + (R.SP_START, R.SP_STAR_END): has_block & Has(KeyItem.BELL) & has_claw, + (R.SP_START, R.SP_CAMPFIRE_1): HasSwitch(Crystal.SP_BLOCKS) | otherwise_crystal, (R.SP_CAMPFIRE_1, R.SP_HEARTS): HasSwitch(Switch.SP_BUBBLES, otherwise=True), (R.SP_HEARTS, R.SP_CAMPFIRE_1): HasSwitch(Switch.SP_BUBBLES), - (R.SP_HEARTS, R.SP_ORBS): HasAll(KeyItem.STAR, KeyItem.BELL, Character.KYULI), + (R.SP_HEARTS, R.SP_ORBS): has_star & Has(KeyItem.BELL) & has_kyuli, (R.SP_HEARTS, R.SP_FROG): HasSwitch(Switch.SP_DOUBLE_DOORS, otherwise=True), - (R.SP_PAINTING, R.SP_HEARTS): And(HasAll(KeyItem.BELL, ShopUpgrade.ALGUS_METEOR), chalice_on_easy), - (R.SP_PAINTING, R.SP_SHAFT): And(Has(KeyItem.CLAW), HasBlue(BlueDoor.SP, otherwise=True)), + (R.SP_PAINTING, R.SP_HEARTS): Has(KeyItem.BELL) & has_algus_meteor & chalice_on_easy, + (R.SP_PAINTING, R.SP_SHAFT): has_claw & HasBlue(BlueDoor.SP, otherwise=True), (R.SP_SHAFT, R.SP_PAINTING): HasBlue(BlueDoor.SP, otherwise=True), - (R.SP_SHAFT, R.SP_STAR): And( - HasAll(KeyItem.CLAW, KeyItem.BELL), - Or(HasSwitch(Crystal.SP_STAR), otherwise_crystal), - ), - (R.SP_STAR, R.SP_SHAFT): And( - HasAll(KeyItem.BELL, ShopUpgrade.ALGUS_METEOR), - chalice_on_easy, - HasSwitch(Crystal.SP_STAR), - ), - (R.SP_STAR, R.SP_STAR_CONNECTION): Has(KeyItem.STAR), - (R.SP_STAR_CONNECTION, R.SP_STAR): Has(KeyItem.STAR), - (R.SP_STAR_CONNECTION, R.SP_STAR_END): And( - Has(KeyItem.STAR), - Or(HasSwitch(Switch.SP_AFTER_STAR), Has(Character.ARIAS, options=switch_off)), - ), - (R.SP_STAR_END, R.SP_STAR_CONNECTION): And(Has(KeyItem.STAR), HasSwitch(Switch.SP_AFTER_STAR)), + (R.SP_SHAFT, R.SP_STAR): has_claw & Has(KeyItem.BELL) & (HasSwitch(Crystal.SP_STAR) | otherwise_crystal), + (R.SP_STAR, R.SP_SHAFT): Has(KeyItem.BELL) & has_algus_meteor & chalice_on_easy & HasSwitch(Crystal.SP_STAR), + (R.SP_STAR, R.SP_STAR_CONNECTION): has_star, + (R.SP_STAR_CONNECTION, R.SP_STAR): has_star, + (R.SP_STAR_CONNECTION, R.SP_STAR_END): has_star & (HasSwitch(Switch.SP_AFTER_STAR) | (has_arias & switch_off)), + (R.SP_STAR_END, R.SP_STAR_CONNECTION): has_star & HasSwitch(Switch.SP_AFTER_STAR), } -MAIN_LOCATION_RULES: dict[L, RuleFactory] = { - L.GT_GORGONHEART: Or( - HasSwitch(Switch.GT_GH, otherwise=True), - HasAny(Character.KYULI, KeyItem.ICARUS, KeyItem.BLOCK, KeyItem.CLOAK, KeyItem.BOOTS), +MAIN_LOCATION_RULES: dict[L, Rule[AstalonWorldBase]] = { + L.GT_GORGONHEART: ( + HasSwitch(Switch.GT_GH, otherwise=True) | has_kyuli | has_boots | has_block | has_cloak | Has(KeyItem.ICARUS) ), L.GT_ANCIENTS_RING: Has(Eye.RED), - L.GT_BANISH: And( - CanReachRegion(R.GT_BOTTOM), - CanReachRegion(R.GT_ASCENDANT_KEY), - CanReachRegion(R.GT_BUTT), - HasAny(Character.ALGUS, Character.KYULI, Character.BRAM, Character.ZEEK, KeyItem.SWORD), + L.GT_BANISH: ( + CanReachRegion(R.GT_BOTTOM) + & CanReachRegion(R.GT_ASCENDANT_KEY) + & CanReachRegion(R.GT_BUTT) + & (has_algus | has_kyuli | has_bram | has_zeek | has_sword) ), - L.HOTP_BELL: Or(HasSwitch(Switch.HOTP_BELL, otherwise=True), Has(Character.KYULI), can_combo_height), + L.HOTP_BELL: HasSwitch(Switch.HOTP_BELL, otherwise=True) | has_kyuli | can_combo_height, L.HOTP_CLAW: can_extra_height, - L.HOTP_MAIDEN_RING: Or(HasSwitch(Crystal.HOTP_MAIDEN_1, Crystal.HOTP_MAIDEN_2), otherwise_crystal), - L.TR_ADORNED_KEY: Or( - HasSwitch(Switch.TR_ADORNED_L, Switch.TR_ADORNED_M, Switch.TR_ADORNED_R), - And( - HasAll(KeyItem.CLAW, Eye.RED, Character.ZEEK, KeyItem.BELL), - CanReachRegion(R.TR_BOTTOM), - CanReachRegion(R.TR_LEFT), - CanReachRegion(R.TR_DARK_ARIAS), + L.HOTP_MAIDEN_RING: HasSwitch(Crystal.HOTP_MAIDEN_1, Crystal.HOTP_MAIDEN_2) | otherwise_crystal, + L.TR_ADORNED_KEY: ( + HasSwitch(Switch.TR_ADORNED_L, Switch.TR_ADORNED_M, Switch.TR_ADORNED_R) + | Filtered( + has_claw + & has_zeek + & HasAll(Eye.RED, KeyItem.BELL) + & CanReachRegion(R.TR_BOTTOM) + & CanReachRegion(R.TR_LEFT) + & CanReachRegion(R.TR_DARK_ARIAS), options=switch_off, - ), + ) ), - L.CATH_BLOCK: Or(HasSwitch(Crystal.CATH_TOP_L, Crystal.CATH_TOP_R), otherwise_crystal), + L.CATH_BLOCK: HasSwitch(Crystal.CATH_TOP_L, Crystal.CATH_TOP_R) | otherwise_crystal, L.MECH_ZEEK: Has(KeyItem.CROWN), - L.MECH_ATTACK_VOLANTIS: Has(KeyItem.CLAW), - L.MECH_ATTACK_STAR: Has(KeyItem.STAR), - L.ROA_ATTACK: And(HasAll(KeyItem.STAR, KeyItem.BELL), can_extra_height), + L.MECH_ATTACK_VOLANTIS: has_claw, + L.MECH_ATTACK_STAR: has_star, + L.ROA_ATTACK: has_star & Has(KeyItem.BELL) & can_extra_height, L.CAVES_ATTACK_RED: Has(Eye.RED), L.CAVES_ATTACK_BLUE: HasAll(Eye.RED, Eye.BLUE), - L.CAVES_ATTACK_GREEN: And(HasAll(Eye.RED, Eye.BLUE), HasAny(Eye.GREEN, KeyItem.STAR)), - L.CD_ATTACK: Or( - HasSwitch(Switch.CD_TOP, otherwise=True), - HasAll(KeyItem.BLOCK, KeyItem.BELL, Character.KYULI), - ), - L.GT_HP_1_RING: Or( - Has(KeyItem.STAR), - And(CanReachRegion(R.GT_UPPER_PATH), HasBlue(BlueDoor.GT_RING, otherwise=True)), - ), - L.GT_HP_5_KEY: Has(KeyItem.CLAW), + L.CAVES_ATTACK_GREEN: HasAll(Eye.RED, Eye.BLUE) & (Has(Eye.GREEN) | has_star), + L.CD_ATTACK: HasSwitch(Switch.CD_TOP, otherwise=True) | (has_block & Has(KeyItem.BELL) & has_kyuli), + L.GT_HP_1_RING: has_star | (CanReachRegion(R.GT_UPPER_PATH) & HasBlue(BlueDoor.GT_RING, otherwise=True)), + L.GT_HP_5_KEY: has_claw, L.MECH_HP_1_SWITCH: HasSwitch(Switch.MECH_INVISIBLE, otherwise=True), - L.MECH_HP_3_CLAW: Has(KeyItem.CLAW), - L.HOTP_HP_2_GAUNTLET: HasAll(KeyItem.CLAW, Character.ZEEK, KeyItem.BELL), - L.HOTP_HP_5_OLD_MAN: And( - Has(KeyItem.CLAW), - Or(And(Has(KeyItem.BELL), can_kill_ghosts), Has(KeyItem.CHALICE)), - HasSwitch(Switch.HOTP_ABOVE_OLD_MAN, otherwise=True), - ), - L.HOTP_HP_5_START: And(Has(KeyItem.CLAW), HasBlue(BlueDoor.HOTP_START, otherwise=True)), - L.ROA_HP_2_RIGHT: And( - HasAny(KeyItem.GAUNTLET, KeyItem.CHALICE, KeyItem.STAR), - HasAll(KeyItem.BELL, Character.KYULI), - Or(HasSwitch(Crystal.ROA_BRANCH_L, Crystal.ROA_BRANCH_R), otherwise_crystal), - ), - L.ROA_HP_5_SOLARIA: Has(Character.KYULI), + L.MECH_HP_3_CLAW: has_claw, + L.HOTP_HP_2_GAUNTLET: has_claw & has_zeek & Has(KeyItem.BELL), + L.HOTP_HP_5_OLD_MAN: ( + has_claw + & ((Has(KeyItem.BELL) & can_kill_ghosts) | Has(KeyItem.CHALICE)) + & HasSwitch(Switch.HOTP_ABOVE_OLD_MAN, otherwise=True) + ), + L.HOTP_HP_5_START: has_claw & HasBlue(BlueDoor.HOTP_START, otherwise=True), + L.ROA_HP_2_RIGHT: ( + (has_gauntlet | Has(KeyItem.CHALICE) | has_star) + & Has(KeyItem.BELL) + & has_kyuli + & (HasSwitch(Crystal.ROA_BRANCH_L, Crystal.ROA_BRANCH_R) | otherwise_crystal) + ), + L.ROA_HP_5_SOLARIA: has_kyuli, L.APEX_HP_1_CHALICE: HasBlue(BlueDoor.APEX, otherwise=True), - L.APEX_HP_5_HEART: HasAny(Character.KYULI, KeyItem.BLOCK), - L.CAVES_HP_1_START: Or(Has(KeyItem.CHALICE), HasSwitch(Face.CAVES_1ST_ROOM), otherwise_bow), - L.CATA_HP_1_ABOVE_POISON: And( - Has(Character.KYULI), - Or( - HasSwitch(Crystal.CATA_POISON_ROOTS), - And(can_crystal, Has(KeyItem.BELL), options=switch_off), - HardLogic(HasAll(KeyItem.ICARUS, KeyItem.CLAW)), - ), - ), - L.CATA_HP_2_GEMINI_BOTTOM: And(Has(Character.KYULI), Or(HasSwitch(Face.CATA_BOTTOM), otherwise_bow)), - L.CATA_HP_2_GEMINI_TOP: Has(Character.KYULI), - L.CATA_HP_2_ABOVE_GEMINI: And( - Or(Has(KeyItem.CLAW), HasAll(KeyItem.BLOCK, KeyItem.BELL)), - Or(HasAll(KeyItem.GAUNTLET, KeyItem.BELL), Has(KeyItem.CHALICE)), - ), - L.CAVES_HP_5_CHAIN: HasAll(Eye.RED, Eye.BLUE, KeyItem.STAR, KeyItem.CLAW, KeyItem.BELL), - L.CD_HP_1: Or( - HasSwitch(Switch.CD_TOP, otherwise=True), - HasAll(KeyItem.BLOCK, KeyItem.BELL, Character.KYULI), - ), - L.CATH_HP_1_TOP_LEFT: HasAny(KeyItem.CLOAK, KeyItem.ICARUS), - L.CATH_HP_1_TOP_RIGHT: HasAny(KeyItem.CLOAK, KeyItem.ICARUS), - L.CATH_HP_2_CLAW: Has(KeyItem.CLAW), - L.CATH_HP_5_BELL: HasAny(Character.KYULI, KeyItem.BLOCK, KeyItem.ICARUS, KeyItem.CLOAK), + L.APEX_HP_5_HEART: has_kyuli | has_block, + L.CAVES_HP_1_START: Has(KeyItem.CHALICE) | HasSwitch(Face.CAVES_1ST_ROOM) | otherwise_bow, + L.CATA_HP_1_ABOVE_POISON: ( + has_kyuli + & ( + HasSwitch(Crystal.CATA_POISON_ROOTS) + | Filtered(can_crystal & Has(KeyItem.BELL), options=switch_off) + | HardLogic(Has(KeyItem.ICARUS) & has_claw) + ) + ), + L.CATA_HP_2_GEMINI_BOTTOM: has_kyuli & (HasSwitch(Face.CATA_BOTTOM) | otherwise_bow), + L.CATA_HP_2_GEMINI_TOP: has_kyuli, + L.CATA_HP_2_ABOVE_GEMINI: ( + (has_claw | (has_block & Has(KeyItem.BELL))) & ((has_gauntlet & Has(KeyItem.BELL)) | Has(KeyItem.CHALICE)) + ), + L.CAVES_HP_5_CHAIN: HasAll(Eye.RED, Eye.BLUE, KeyItem.BELL) & has_star & has_claw, + L.CD_HP_1: HasSwitch(Switch.CD_TOP, otherwise=True) | (has_block & Has(KeyItem.BELL) & has_kyuli), + L.CATH_HP_1_TOP_LEFT: has_cloak | Has(KeyItem.ICARUS), + L.CATH_HP_1_TOP_RIGHT: has_cloak | Has(KeyItem.ICARUS), + L.CATH_HP_2_CLAW: has_claw, + L.CATH_HP_5_BELL: has_kyuli | has_block | Has(KeyItem.ICARUS) | has_cloak, L.MECH_WHITE_KEY_LINUS: HasSwitch(Switch.MECH_LOWER_KEY, otherwise=True), - L.MECH_WHITE_KEY_TOP: And(Or(HasSwitch(Crystal.MECH_TOP), otherwise_crystal), can_extra_height), + L.MECH_WHITE_KEY_TOP: (HasSwitch(Crystal.MECH_TOP) | otherwise_crystal) & can_extra_height, L.ROA_WHITE_KEY_SAVE: HasSwitch(Switch.ROA_WORMS, otherwise=True), - L.CATA_WHITE_KEY_PRISON: Or(can_extra_height, HasAny(KeyItem.CLOAK, KeyItem.ICARUS)), + L.CATA_WHITE_KEY_PRISON: can_extra_height | has_cloak | Has(KeyItem.ICARUS), L.MECH_BLUE_KEY_BLOCKS: HasSwitch(Switch.MECH_KEY_BLOCKS, otherwise=True), - L.MECH_BLUE_KEY_SAVE: Has(KeyItem.CLAW), - L.MECH_BLUE_KEY_POT: Or(Has(Character.KYULI), can_combo_height), - L.HOTP_BLUE_KEY_STATUE: Has(KeyItem.CLAW), - L.HOTP_BLUE_KEY_AMULET: Or(Has(Character.KYULI), can_combo_height), + L.MECH_BLUE_KEY_SAVE: has_claw, + L.MECH_BLUE_KEY_POT: has_kyuli | can_combo_height, + L.HOTP_BLUE_KEY_STATUE: has_claw, + L.HOTP_BLUE_KEY_AMULET: has_kyuli | can_combo_height, L.HOTP_BLUE_KEY_LADDER: can_extra_height, - L.HOTP_BLUE_KEY_MAZE: Or(HasSwitch(Crystal.HOTP_BELOW_PUZZLE), otherwise_crystal), - L.ROA_BLUE_KEY_FACE: Or(HasSwitch(Face.ROA_BLUE_KEY), otherwise_bow), - L.ROA_BLUE_KEY_FLAMES: Or( - HasAll(KeyItem.BLOCK, Character.KYULI, KeyItem.BELL), - CanReachEntrance(R.ROA_FLAMES, R.ROA_ARIAS_BABY_GORGON), + L.HOTP_BLUE_KEY_MAZE: HasSwitch(Crystal.HOTP_BELOW_PUZZLE) | otherwise_crystal, + L.ROA_BLUE_KEY_FACE: HasSwitch(Face.ROA_BLUE_KEY) | otherwise_bow, + L.ROA_BLUE_KEY_FLAMES: ( + (has_block & has_kyuli & Has(KeyItem.BELL)) | CanReachEntrance(R.ROA_FLAMES, R.ROA_ARIAS_BABY_GORGON) ), L.ROA_BLUE_KEY_TOP: can_extra_height, - L.SP_BLUE_KEY_ARIAS: Has(Character.ARIAS), - L.GT_RED_KEY: HasAll(Character.ZEEK, Character.KYULI), - L.ROA_RED_KEY: HasAll(KeyItem.CLOAK, KeyItem.CLAW, KeyItem.BELL), - L.TR_RED_KEY: Has(KeyItem.CLAW), + L.SP_BLUE_KEY_ARIAS: has_arias, + L.GT_RED_KEY: has_zeek & has_kyuli, + L.ROA_RED_KEY: has_cloak & has_claw & Has(KeyItem.BELL), + L.TR_RED_KEY: has_claw, L.SHOP_GIFT: shop_moderate, L.SHOP_KNOWLEDGE: shop_cheap, L.SHOP_MERCY: shop_expensive, @@ -1199,120 +987,107 @@ L.SHOP_BRAM_WHIPLASH: shop_moderate, L.GT_SWITCH_2ND_ROOM: HasWhite(WhiteDoor.GT_START, otherwise=True), L.GT_SWITCH_BUTT_ACCESS: can_extra_height, - L.GT_SWITCH_UPPER_PATH_ACCESS: Or( - HasSwitch(Switch.GT_UPPER_PATH_BLOCKS, otherwise=True), - HasAll(Character.KYULI, KeyItem.BLOCK, Character.ZEEK, KeyItem.BELL), - ), - L.GT_CRYSTAL_ROTA: And( - can_crystal, - Or( - Has(KeyItem.BELL), - And( - CanReachEntrance(R.MECH_BOTTOM_CAMPFIRE, R.GT_UPPER_PATH_CONNECTION), - CanReachEntrance(R.GT_UPPER_PATH_CONNECTION, R.GT_UPPER_PATH), - ), - ), - ), - L.GT_CRYSTAL_OLD_MAN_1: And( - can_crystal, - Or( - Has(KeyItem.BELL), - HasSwitch(Switch.GT_UPPER_ARIAS), - CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off), - ), - ), - L.GT_CRYSTAL_OLD_MAN_2: And( - can_crystal, - HasSwitch(Crystal.GT_OLD_MAN_1, otherwise=True), - Or( - Has(KeyItem.BELL), - HasSwitch(Switch.GT_UPPER_ARIAS), - CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off), - ), - ), - L.MECH_SWITCH_BOOTS_ACCESS: HasAny(Eye.RED, KeyItem.STAR), - L.MECH_SWITCH_UPPER_VOID_DROP: Has(KeyItem.CLAW), - L.MECH_SWITCH_CANNON: Or(HasSwitch(Crystal.MECH_CANNON), otherwise_crystal), - L.MECH_SWITCH_ARIAS: Has(Character.ARIAS), + L.GT_SWITCH_UPPER_PATH_ACCESS: ( + HasSwitch(Switch.GT_UPPER_PATH_BLOCKS, otherwise=True) | (has_kyuli & has_block & has_zeek & Has(KeyItem.BELL)) + ), + L.GT_CRYSTAL_ROTA: ( + can_crystal + & ( + Has(KeyItem.BELL) + | ( + CanReachEntrance(R.MECH_BOTTOM_CAMPFIRE, R.GT_UPPER_PATH_CONNECTION) + & CanReachEntrance(R.GT_UPPER_PATH_CONNECTION, R.GT_UPPER_PATH) + ) + ) + ), + L.GT_CRYSTAL_OLD_MAN_1: ( + can_crystal + & ( + Has(KeyItem.BELL) + | HasSwitch(Switch.GT_UPPER_ARIAS) + | CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off) + ) + ), + L.GT_CRYSTAL_OLD_MAN_2: ( + can_crystal + & HasSwitch(Crystal.GT_OLD_MAN_1, otherwise=True) + & ( + Has(KeyItem.BELL) + | HasSwitch(Switch.GT_UPPER_ARIAS) + | CanReachRegion(R.GT_ARIAS_SWORD_SWITCH, options=switch_off) + ) + ), + L.MECH_SWITCH_BOOTS_ACCESS: Has(Eye.RED) | has_star, + L.MECH_SWITCH_UPPER_VOID_DROP: has_claw, + L.MECH_SWITCH_CANNON: HasSwitch(Crystal.MECH_CANNON) | otherwise_crystal, + L.MECH_SWITCH_ARIAS: has_arias, L.MECH_CRYSTAL_CANNON: can_crystal, L.MECH_CRYSTAL_LINUS: can_crystal, L.MECH_CRYSTAL_LOWER: can_crystal, L.MECH_CRYSTAL_TO_BOSS_3: can_crystal, L.MECH_CRYSTAL_TOP: can_crystal, - L.MECH_CRYSTAL_CLOAK: And(can_crystal, Has(Eye.BLUE)), + L.MECH_CRYSTAL_CLOAK: can_crystal & Has(Eye.BLUE), L.MECH_CRYSTAL_SLIMES: can_crystal, - L.MECH_CRYSTAL_TO_CD: And(can_crystal, Has(Eye.BLUE), HasBlue(BlueDoor.MECH_CD, otherwise=True)), + L.MECH_CRYSTAL_TO_CD: can_crystal & Has(Eye.BLUE) & HasBlue(BlueDoor.MECH_CD, otherwise=True), L.MECH_CRYSTAL_CAMPFIRE: can_crystal, L.MECH_CRYSTAL_1ST_ROOM: can_crystal, L.MECH_CRYSTAL_OLD_MAN: can_crystal, L.MECH_CRYSTAL_TOP_CHAINS: can_crystal, L.MECH_CRYSTAL_BK: can_crystal, - L.MECH_FACE_ABOVE_VOLANTIS: HasAll(KeyItem.BOW, KeyItem.CLAW), - L.HOTP_SWITCH_LOWER_SHORTCUT: Or(HasSwitch(Crystal.HOTP_LOWER), otherwise_crystal), - L.HOTP_SWITCH_TO_CLAW_2: Or( - HasSwitch(Switch.HOTP_TO_CLAW_1, otherwise=True), - And(HasSwitch(Switch.HOTP_TO_CLAW_2, otherwise=True), can_extra_height), - Has(KeyItem.CLAW), - ), - L.HOTP_SWITCH_CLAW_ACCESS: Or(Has(Character.KYULI), can_block_in_wall), - L.HOTP_SWITCH_LEFT_3: Or( - HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True), - And(Has(KeyItem.STAR), CanReachRegion(R.HOTP_START_LEFT)), + L.MECH_FACE_ABOVE_VOLANTIS: has_bow & has_claw, + L.HOTP_SWITCH_LOWER_SHORTCUT: HasSwitch(Crystal.HOTP_LOWER) | otherwise_crystal, + L.HOTP_SWITCH_TO_CLAW_2: ( + HasSwitch(Switch.HOTP_TO_CLAW_1, otherwise=True) + | (HasSwitch(Switch.HOTP_TO_CLAW_2, otherwise=True) & can_extra_height) + | has_claw + ), + L.HOTP_SWITCH_CLAW_ACCESS: has_kyuli | can_block_in_wall, + L.HOTP_SWITCH_LEFT_3: ( + HasSwitch(Switch.HOTP_LEFT_1, Switch.HOTP_LEFT_2, otherwise=True) + | (has_star & CanReachRegion(R.HOTP_START_LEFT)) ), L.HOTP_CRYSTAL_ROCK_ACCESS: can_crystal, L.HOTP_CRYSTAL_BOTTOM: can_crystal, L.HOTP_CRYSTAL_LOWER: can_crystal, L.HOTP_CRYSTAL_AFTER_CLAW: can_crystal, L.HOTP_CRYSTAL_MAIDEN_1: can_crystal, - L.HOTP_CRYSTAL_MAIDEN_2: And( - can_crystal, - Or(HasSwitch(Crystal.HOTP_MAIDEN_1, otherwise=True), Has(Character.KYULI)), - ), + L.HOTP_CRYSTAL_MAIDEN_2: can_crystal & (HasSwitch(Crystal.HOTP_MAIDEN_1, otherwise=True) | has_kyuli), L.HOTP_CRYSTAL_BELL_ACCESS: can_crystal, L.HOTP_CRYSTAL_HEART: can_crystal, L.HOTP_CRYSTAL_BELOW_PUZZLE: can_crystal, - L.HOTP_FACE_OLD_MAN: Has(KeyItem.BOW), - L.ROA_SWITCH_SPIKE_CLIMB: Has(KeyItem.CLAW), - L.ROA_SWITCH_TRIPLE_3: Or(HasSwitch(Crystal.ROA_TRIPLE_2), otherwise_crystal), - L.ROA_CRYSTAL_1ST_ROOM: And(can_crystal, HasAll(Character.KYULI, KeyItem.BELL)), + L.HOTP_FACE_OLD_MAN: has_bow, + L.ROA_SWITCH_SPIKE_CLIMB: has_claw, + L.ROA_SWITCH_TRIPLE_3: HasSwitch(Crystal.ROA_TRIPLE_2) | otherwise_crystal, + L.ROA_CRYSTAL_1ST_ROOM: can_crystal & has_kyuli & Has(KeyItem.BELL), L.ROA_CRYSTAL_BABY_GORGON: can_crystal, - L.ROA_CRYSTAL_LADDER_R: And( - can_crystal_wo_whiplash, - Or(Has(KeyItem.BELL), HardLogic(Has(ShopUpgrade.KYULI_RAY))), - ), - L.ROA_CRYSTAL_LADDER_L: And( - can_crystal_wo_whiplash, - Or(Has(KeyItem.BELL), HardLogic(Has(ShopUpgrade.KYULI_RAY))), - ), - L.ROA_CRYSTAL_CENTAUR: And(can_crystal, HasAll(KeyItem.BELL, Character.ARIAS)), + L.ROA_CRYSTAL_LADDER_R: can_crystal_no_whiplash & (Has(KeyItem.BELL) | HardLogic(has_kyuli_ray)), + L.ROA_CRYSTAL_LADDER_L: can_crystal_no_whiplash & (Has(KeyItem.BELL) | HardLogic(has_kyuli_ray)), + L.ROA_CRYSTAL_CENTAUR: can_crystal & Has(KeyItem.BELL) & has_arias, L.ROA_CRYSTAL_SPIKE_BALLS: can_crystal, L.ROA_CRYSTAL_SHAFT: can_crystal, - L.ROA_CRYSTAL_BRANCH_R: And(can_crystal, HasAll(Character.KYULI, KeyItem.BELL)), - L.ROA_CRYSTAL_BRANCH_L: And(can_crystal, HasAll(Character.KYULI, KeyItem.BELL)), + L.ROA_CRYSTAL_BRANCH_R: can_crystal & has_kyuli & Has(KeyItem.BELL), + L.ROA_CRYSTAL_BRANCH_L: can_crystal & has_kyuli & Has(KeyItem.BELL), L.ROA_CRYSTAL_3_REAPERS: can_crystal, - L.ROA_CRYSTAL_TRIPLE_2: And(can_crystal, HasSwitch(Switch.ROA_TRIPLE_1, otherwise=True)), - L.ROA_FACE_SPIDERS: Has(KeyItem.BOW), - L.ROA_FACE_BLUE_KEY: Has(KeyItem.BOW), - L.DARK_SWITCH: Has(KeyItem.CLAW), - L.CAVES_FACE_1ST_ROOM: Has(KeyItem.BOW), + L.ROA_CRYSTAL_TRIPLE_2: can_crystal & HasSwitch(Switch.ROA_TRIPLE_1, otherwise=True), + L.ROA_FACE_SPIDERS: has_bow, + L.ROA_FACE_BLUE_KEY: has_bow, + L.DARK_SWITCH: has_claw, + L.CAVES_FACE_1ST_ROOM: has_bow, L.CATA_SWITCH_CLAW_2: HasSwitch(Switch.CATA_CLAW_1, otherwise=True), L.CATA_SWITCH_FLAMES_2: HasSwitch(Switch.CATA_FLAMES_1, otherwise=True), L.CATA_CRYSTAL_POISON_ROOTS: can_crystal, - L.CATA_FACE_AFTER_BOW: Has(KeyItem.BOW), - L.CATA_FACE_BOW: Has(KeyItem.BOW), - L.CATA_FACE_X4: Has(KeyItem.BOW), - L.CATA_FACE_CAMPFIRE: Has(KeyItem.BOW), - L.CATA_FACE_DOUBLE_DOOR: Has(KeyItem.BOW), - L.CATA_FACE_BOTTOM: Has(KeyItem.BOW), - L.TR_SWITCH_ADORNED_L: Has(KeyItem.CLAW), + L.CATA_FACE_AFTER_BOW: has_bow, + L.CATA_FACE_BOW: has_bow, + L.CATA_FACE_X4: has_bow, + L.CATA_FACE_CAMPFIRE: has_bow, + L.CATA_FACE_DOUBLE_DOOR: has_bow, + L.CATA_FACE_BOTTOM: has_bow, + L.TR_SWITCH_ADORNED_L: has_claw, L.TR_SWITCH_ADORNED_M: Has(Eye.RED), - L.TR_SWITCH_ADORNED_R: And( - HasSwitch(Crystal.TR_DARK_ARIAS, otherwise=True), - HasAll(Character.ZEEK, KeyItem.BELL, KeyItem.CLAW), - ), - L.TR_CRYSTAL_GOLD: And(can_crystal, HasAll(KeyItem.BELL, KeyItem.CLAW)), - L.TR_CRYSTAL_DARK_ARIAS: And(can_crystal, HasAll(Character.ZEEK, KeyItem.BELL, KeyItem.CLAW)), - L.CD_SWITCH_1: Or(HasSwitch(Crystal.CD_START), otherwise_crystal), + L.TR_SWITCH_ADORNED_R: HasSwitch(Crystal.TR_DARK_ARIAS, otherwise=True) & has_zeek & Has(KeyItem.BELL) & has_claw, + L.TR_CRYSTAL_GOLD: can_crystal & Has(KeyItem.BELL) & has_claw, + L.TR_CRYSTAL_DARK_ARIAS: can_crystal & has_zeek & Has(KeyItem.BELL) & has_claw, + L.CD_SWITCH_1: HasSwitch(Crystal.CD_START) | otherwise_crystal, L.CD_CRYSTAL_BACKTRACK: can_crystal, L.CD_CRYSTAL_START: can_crystal, L.CD_CRYSTAL_CAMPFIRE: can_crystal, @@ -1324,18 +1099,16 @@ L.CATH_CRYSTAL_TOP_R: can_crystal, L.CATH_CRYSTAL_SHAFT_ACCESS: can_crystal, L.CATH_CRYSTAL_ORBS: can_crystal, - L.CATH_FACE_LEFT: Has(KeyItem.BOW), - L.CATH_FACE_RIGHT: Has(KeyItem.BOW), - L.SP_SWITCH_AFTER_STAR: Has(Character.ARIAS), + L.CATH_FACE_LEFT: has_bow, + L.CATH_FACE_RIGHT: has_bow, + L.SP_SWITCH_AFTER_STAR: has_arias, L.SP_CRYSTAL_BLOCKS: can_crystal, L.SP_CRYSTAL_STAR: can_crystal, - L.ROA_CANDLE_ARENA: Or( - can_extra_height, - Has(ShopUpgrade.BRAM_AXE), - CanReachRegion(R.ROA_FLAMES_CONNECTION), - ), - L.ROA_CANDLE_HIDDEN_4: HasAny(Character.KYULI, ShopUpgrade.BRAM_AXE), - L.ROA_CANDLE_HIDDEN_5: Has(Character.KYULI), - L.CATA_CANDLE_DEV_ROOM: Or(Has(KeyItem.CLAW), HasSwitch(Switch.CATA_DEV_ROOM, otherwise=True)), + L.ROA_CANDLE_ARENA: can_extra_height | has_bram_axe | CanReachRegion(R.ROA_FLAMES_CONNECTION), + L.ROA_CANDLE_HIDDEN_4: has_kyuli | has_bram_axe, + L.ROA_CANDLE_HIDDEN_5: has_kyuli, + L.CATA_CANDLE_DEV_ROOM: has_claw | HasSwitch(Switch.CATA_DEV_ROOM, otherwise=True), L.CATA_CANDLE_PRISON: HasBlue(BlueDoor.CATA_PRISON_RIGHT, otherwise=True), } + +COMPLETION_RULE: Rule[AstalonWorldBase] = Has(Events.VICTORY) diff --git a/worlds/astalon/logic/mixin.py b/worlds/astalon/logic/mixin.py deleted file mode 100644 index 3dd526c7adcb..000000000000 --- a/worlds/astalon/logic/mixin.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import TYPE_CHECKING - -from worlds.AutoWorld import LogicMixin - -from ..constants import GAME_NAME - -if TYPE_CHECKING: - from BaseClasses import CollectionState, MultiWorld -else: - CollectionState = object - - -class AstalonLogicMixin(LogicMixin, CollectionState): - multiworld: "MultiWorld" - - _astalon_rule_results: dict[int, dict[int, bool]] - - def init_mixin(self, multiworld: "MultiWorld") -> None: - players = multiworld.get_game_players(GAME_NAME) - self._astalon_rule_results = {player: {} for player in players} - - def copy_mixin(self, new_state: "AstalonLogicMixin") -> "AstalonLogicMixin": - new_state._astalon_rule_results = { - player: rule_results.copy() for player, rule_results in self._astalon_rule_results.items() - } - return new_state diff --git a/worlds/astalon/regions.py b/worlds/astalon/regions.py index 5d8fd03bdf0e..fa458ebcf89e 100644 --- a/worlds/astalon/regions.py +++ b/worlds/astalon/regions.py @@ -803,6 +803,7 @@ class RegionData: ), RegionName.HOTP_BELL_CAMPFIRE: RegionData( exits=( + RegionName.HOTP_TP_TUTORIAL, RegionName.HOTP_RED_KEY, RegionName.HOTP_BELL, RegionName.HOTP_CATH_CONNECTION, diff --git a/worlds/astalon/test/test_rules.py b/worlds/astalon/test/test_rules.py deleted file mode 100644 index 08fef9b3dcf1..000000000000 --- a/worlds/astalon/test/test_rules.py +++ /dev/null @@ -1,71 +0,0 @@ -from ..items import BlueDoor, Character, Crystal, KeyItem -from ..logic.factories import And, Has, HasAny, HasBlue, HasSwitch, Or, True_ -from ..logic.instances import HasAllInstance, HasInstance, OrInstance -from .bases import AstalonTestBase - - -class RuleHashTest(AstalonTestBase): - auto_construct = False - - @property - def run_default_tests(self) -> bool: - return False - - def test_same_rules_have_same_hash(self) -> None: - rule1 = HasInstance("Item", player=1) - rule2 = HasInstance("Item", player=1) - self.assertEqual(hash(rule1), hash(rule2)) - - def test_different_rules_have_different_hashes(self) -> None: - rule1 = HasInstance("Item", player=1) - rule2 = HasInstance("Item", player=2) - self.assertNotEqual(hash(rule1), hash(rule2)) - - rule3 = HasInstance("Item1", player=1) - rule4 = HasInstance("Item2", player=1) - self.assertNotEqual(hash(rule3), hash(rule4)) - - -class RuleResolutionTest(AstalonTestBase): - options = { # noqa: RUF012 - "difficulty": "easy", - "randomize_characters": "trio", - "randomize_key_items": "true", - "randomize_health_pickups": "true", - "randomize_attack_pickups": "true", - "randomize_white_keys": "true", - "randomize_blue_keys": "true", - "randomize_red_keys": "true", - "randomize_shop": "true", - "randomize_elevator": "true", - "randomize_switches": "true", - } - - @property - def run_default_tests(self) -> bool: - return False - - def test_upper_path_rule_easy(self) -> None: - rule = Or( - HasSwitch(Crystal.GT_ROTA), - Or( - True_(options={"randomize_characters": 0}), - HasAny(Character.ARIAS, Character.BRAM, options={"randomize_characters__ge": 1}), - options={"difficulty": 1}, - ), - And(Has(KeyItem.STAR), HasBlue(BlueDoor.GT_RING, otherwise=True)), - Has(KeyItem.BLOCK), - ) - expected = OrInstance( - ( - HasAllInstance( - ("Blue Door (Gorgon Tomb - Ring of the Ancients)", "Bram", "Morning Star"), - player=self.player, - ), - HasAllInstance(("Zeek", "Magic Block"), player=self.player), - HasInstance("Crystal (Gorgon Tomb - RotA)", player=self.player), - ), - player=self.player, - ) - instance = rule.resolve(self.world) - self.assertEqual(instance, expected) diff --git a/worlds/astalon/tracker.py b/worlds/astalon/tracker.py index fc0d4d894654..7de75976698d 100644 --- a/worlds/astalon/tracker.py +++ b/worlds/astalon/tracker.py @@ -3,14 +3,15 @@ from typing_extensions import override -from BaseClasses import CollectionState, Entrance, Location, Region +from BaseClasses import CollectionRule, CollectionState, Entrance, Location, Region from NetUtils import JSONMessagePart from Options import Option +from rule_builder.rules import Rule from Utils import get_intended_text # pyright: ignore[reportUnknownVariableType] from .bases import AstalonWorldBase from .items import Character, Events -from .logic.instances import CampfireWarpInstance, RuleInstance +from .logic.custom_rules import CampfireWarp from .regions import RegionName @@ -109,11 +110,11 @@ def location_icon_coords(index: int | None, coords: dict[str, Any]) -> tuple[int return x, y, f"images/icons/{icon}.png" -def rule_to_json(rule: RuleInstance | None, state: CollectionState) -> list[JSONMessagePart]: - if rule: +def rule_to_json(rule: CollectionRule | None, state: CollectionState) -> list[JSONMessagePart]: + if isinstance(rule, Rule.Resolved): return [ {"type": "text", "text": " "}, - *rule.explain(state), + *rule.explain_json(state), ] return [ {"type": "text", "text": " "}, @@ -207,7 +208,7 @@ def get_logical_path(self, dest_name: str, state: CollectionState) -> list[JSONM [ {"type": "entrance_name", "text": p.name, "player": self.player}, {"type": "text", "text": "\n"}, - *rule_to_json(getattr(p.access_rule, "__self__", None), state), + *rule_to_json(p.access_rule, state), {"type": "text", "text": "\n"}, ] ) @@ -222,7 +223,7 @@ def get_logical_path(self, dest_name: str, state: CollectionState) -> list[JSONM "player": self.player, }, {"type": "text", "text": "\n"}, - *rule_to_json(getattr(goal_location.access_rule, "__self__", None), state), + *rule_to_json(goal_location.access_rule, state), ] ) @@ -250,5 +251,4 @@ def reconnect_found_entrances(self, found_key: str, data_storage_value: Any) -> except KeyError: pass - rule = CampfireWarpInstance(campfire_name, player=self.player) - source_region.connect(dest_region, rule=rule.test) + self.create_entrance(source_region, dest_region, rule=CampfireWarp(campfire_name)) diff --git a/worlds/astalon/world.py b/worlds/astalon/world.py index 29aa65aecdab..4c8b2405bad9 100644 --- a/worlds/astalon/world.py +++ b/worlds/astalon/world.py @@ -4,7 +4,7 @@ from typing_extensions import override -from BaseClasses import CollectionState, EntranceType, Item, ItemClassification, MultiWorld, Region +from BaseClasses import EntranceType, Item, ItemClassification, MultiWorld, Region from entrance_rando import disconnect_entrance_for_randomization, randomize_entrances from Options import OptionError @@ -35,7 +35,7 @@ location_name_to_id, location_table, ) -from .logic import MAIN_ENTRANCE_RULES, MAIN_LOCATION_RULES +from .logic.main_campaign import COMPLETION_RULE, MAIN_ENTRANCE_RULES, MAIN_LOCATION_RULES from .options import ApexElevator, Goal, RandomizeCharacters, ShuffleVoidPortals, StartingLocation from .regions import DEFAULT_PORTALS, STARTING_REGIONS, RegionName, astalon_regions from .tracker import AstalonUTWorld @@ -51,7 +51,7 @@ # ░▓████▓▓░█████████░██░ MANY GOOD PROGRAMS AND FEW ERRORS WILL COME TO YOU # █░▓██▓▓░███░███░██░▓░█ AS LONG AS YOU KEEP HER IN YOUR PROGRAM TO WATCH OVER IT # ██░░▓▓▓░███░███░██░░██ INCREMENT THIS NUMBER EVERY TIME YOU SAY HI TO BUBSETTE -# ████░░░░██████████░███ hi_bubsette = 3 +# ████░░░░██████████░███ hi_bubsette = 4 # ████████░░░░░░░░░░████ @@ -121,12 +121,7 @@ def create_location(self, name: str) -> AstalonLocation: location = AstalonLocation(self.player, name, location_name_to_id.get(name), region) rule = MAIN_LOCATION_RULES.get(location_name) if rule is not None: - rule = rule.resolve(self) - if rule.always_false: - logger.debug(f"No matching rules for {name}") - for item_name, rules in rule.deps().items(): - self._rule_deps[item_name] |= rules - location.access_rule = rule.test + self.set_rule(location, rule) region.locations.append(location) return location @@ -139,17 +134,10 @@ def create_regions(self) -> None: for region_name, region_data in astalon_regions.items(): region = self.get_region(region_name.value) for exit_region_name in region_data.exits: + exit_region = self.get_region(exit_region_name.value) region_pair = (region_name, exit_region_name) rule = MAIN_ENTRANCE_RULES.get(region_pair) - if rule is not None: - rule = rule.resolve(self) - if rule.always_false: - logger.debug(f"No matching rules for {region_name.value} -> {exit_region_name.value}") - continue - for item_name, rules in rule.deps().items(): - self._rule_deps[item_name] |= rules - exit_region = self.get_region(exit_region_name.value) name = None if ( self.options.shuffle_void_portals @@ -158,13 +146,7 @@ def create_regions(self) -> None: ): name = f"{region_name} Portal" - entrance = region.connect(exit_region, name=name, rule=rule.test if rule else None) - if rule: - for indirect_region in rule.indirect(): - self.multiworld.register_indirect_condition( - self.get_region(indirect_region.value), - entrance, - ) + self.create_entrance(region, exit_region, rule, name) if self.options.shuffle_void_portals: for left_region_name, right_region_name in DEFAULT_PORTALS: @@ -264,10 +246,7 @@ def create_regions(self) -> None: ) victory_location.place_locked_item(victory_item) victory_region.locations.append(victory_location) - self.multiworld.completion_condition[self.player] = lambda state: state.has( - Events.VICTORY.value, - self.player, - ) + self.set_completion_rule(COMPLETION_RULE) @override def create_item(self, name: str) -> AstalonItem: @@ -277,7 +256,7 @@ def create_item(self, name: str) -> AstalonItem: item_data = item_table[name] classification: ItemClassification if callable(item_data.classification): - classification = item_data.classification(self) + classification = item_data.classification(self.options) else: classification = item_data.classification return AstalonItem(name, classification, self.item_name_to_id[name], self.player) @@ -517,21 +496,3 @@ def fill_slot_data(self) -> dict[str, Any]: def stage_modify_multidata(cls, *_) -> None: # Clean up calculated character strengths after generation completes cls._character_strengths = None - - @override - def collect(self, state: "CollectionState", item: "Item") -> bool: - changed = super().collect(state, item) - if changed and getattr(self, "_rule_deps", None): - player_results: dict[int, bool] = state._astalon_rule_results[self.player] # type: ignore - for rule_id in self._rule_deps[item.name]: - player_results.pop(rule_id, None) - return changed - - @override - def remove(self, state: "CollectionState", item: "Item") -> bool: - changed = super().remove(state, item) - if changed and getattr(self, "_rule_deps", None): - player_results: dict[int, bool] = state._astalon_rule_results[self.player] # type: ignore - for rule_id in self._rule_deps[item.name]: - player_results.pop(rule_id, None) - return changed