from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING, Mapping from BaseClasses import CollectionState from worlds.generic.Rules import CollectionRule if TYPE_CHECKING: from . import BlasphemousWorld else: BlasphemousWorld = object # Depending on a player's options, some logic can either always be True, or always be False. # When combining rules together in load_rule(), optimizations can be made by checking whether a rule being combined is # _always or _never. def _always(state: CollectionState): return True def _never(state: CollectionState): return False def _bool_rule(b) -> CollectionRule: """Small helper to return the appropriate rule function for a rule that can be pre-calculated""" if b: return _always else: return _never # Player strengths required to logically beat bosses. # Mapping is an immutable type, so type hints should warn if attempts are made to modify it. BOSS_STRENGTHS: Mapping[str, float] = { "warden": -0.10, "ten-piedad": 0.05, "charred-visage": 0.20, "tres-angustias": 0.15, "esdras": 0.25, "melquiades": 0.25, "exposito": 0.30, "quirce": 0.35, "crisanta": 0.50, "isidora": 0.70, "sierpes": 0.70, "amanecida": 0.60, "laudes": 0.60, "perpetua": -0.05, "legionary": 0.20 } class BlasRules: player: int world: BlasphemousWorld string_rules: Dict[str, Callable[[CollectionState], bool]] upwarp_skips_allowed: bool mourning_skip_allowed: bool enemy_skips_allowed: bool obscure_skips_allowed: bool precise_skips_allowed: bool can_enemy_bounce: bool # Player strengths required to logically beat bosses, adjusted by the player's difficulty option. boss_strengths: Mapping[str, float] can_enemy_upslash: CollectionRule can_air_stall: CollectionRule can_dawn_jump: CollectionRule can_dive_laser: CollectionRule can_survive_poison_1: CollectionRule can_survive_poison_2: CollectionRule can_survive_poison_3: CollectionRule def __init__(self, world: "BlasphemousWorld") -> None: self.player = world.player self.world = world self.multiworld = world.multiworld self.indirect_conditions: List[Tuple[str, str]] = [] difficulty = world.options.difficulty.value # Rules that can be fully or partially pre-calculated based on world.options. # Special Skips self.upwarp_skips_allowed = difficulty >= 2 self.mourning_skip_allowed = difficulty >= 2 self.enemy_skips_allowed = difficulty >= 2 and not world.options.enemy_randomizer.value self.obscure_skips_allowed = difficulty >= 2 self.precise_skips_allowed = difficulty >= 2 if difficulty >= 2: # Beating bosses ends up in logic earlier. self.boss_strengths = {boss: strength - 0.1 for boss, strength in BOSS_STRENGTHS.items()} elif difficulty >= 1: self.boss_strengths = BOSS_STRENGTHS else: # Beating bosses ends up in logic later. self.boss_strengths = {boss: strength + 0.1 for boss, strength in BOSS_STRENGTHS.items()} # Enemy tech if self.enemy_skips_allowed: self.can_enemy_bounce = True self.can_enemy_upslash = lambda state: self.combo(state) >= 2 else: self.can_enemy_bounce = False self.can_enemy_upslash = _never # Movement tech if difficulty >= 1: self.can_air_stall = self.ranged self.can_dawn_jump = lambda state: self.dawn_heart(state) and self.dash(state) else: self.can_air_stall = _never self.can_dawn_jump = _never # Breakable tech if difficulty >= 2: self.can_dive_laser = lambda state: self.dive(state) >= 3 else: self.can_dive_laser = _never # Lung tech if difficulty >= 2: self.can_survive_poison_1 = _always self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state) self.can_survive_poison_3 = lambda state: self.lung(state) or (self.tiento(state) and self.total_fervour(state) >= 120) elif difficulty >= 1: self.can_survive_poison_1 = lambda state: self.lung(state) or self.tiento(state) self.can_survive_poison_2 = lambda state: self.lung(state) or self.tiento(state) self.can_survive_poison_3 = self.lung else: self.can_survive_poison_1 = self.lung self.can_survive_poison_2 = self.lung self.can_survive_poison_3 = self.lung # BrandenEK/Blasphemous.Randomizer/ItemRando/BlasphemousInventory.cs self.string_rules: dict[str, CollectionRule] = { # Visibility flags "DoubleJump": _bool_rule(self.world.options.purified_hand.value), "NormalLogic": _bool_rule(self.world.options.difficulty.value >= 1), "NormalLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 1 and bool(self.world.options.purified_hand.value)), "HardLogic": _bool_rule(self.world.options.difficulty.value >= 2), "HardLogicAndDoubleJump": _bool_rule(self.world.options.difficulty.value >= 2 and bool(self.world.options.purified_hand.value)), "EnemySkips": _bool_rule(self.enemy_skips_allowed), "EnemySkipsAndDoubleJump": _bool_rule(self.enemy_skips_allowed and self.world.options.purified_hand.value), # Relics "blood": self.blood, # skip "root" "linen": self.linen, "nail": self.nail, "shroud": self.shroud, # skip "lung" # Keys "bronzeKey": self.bronze_key, "silverKey": self.silver_key, "goldKey": self.gold_key, "peaksKey": self.peaks_key, "elderKey": self.elder_key, "woodKey": self.wood_key, # Collections "cherubs20": lambda state: self.cherubs(state) >= 20, "cherubs38": lambda state: self.cherubs(state) >= 38, "bones4": lambda state: self.bones(state, 4), "bones8": lambda state: self.bones(state, 8), "bones12": lambda state: self.bones(state, 12), "bones16": lambda state: self.bones(state, 16), "bones20": lambda state: self.bones(state, 20), "bones24": lambda state: self.bones(state, 24), "bones28": lambda state: self.bones(state, 28), "bones30": lambda state: self.bones(state, 30), "bones32": lambda state: self.bones(state, 32), "bones36": lambda state: self.bones(state, 36), "bones40": lambda state: self.bones(state, 40), "bones44": lambda state: self.bones(state, 44), "tears0": _always, # Special items "dash": self.dash, "wallClimb": self.wall_climb, # skip "airImpulse" "boots": self.boots, "doubleJump": self.double_jump, # Speed boosts "wheel": self.wheel, # skip "dawnHeart" # Health boosts # skip "flasks" # skip "quicksilver" # Puzzles "redWax1": lambda state: self.red_wax(state) >= 1, "redWax3": lambda state: self.red_wax(state) >= 3, "blueWax1": lambda state: self.blue_wax(state) >= 1, "blueWax3": lambda state: self.blue_wax(state) >= 3, "chalice": self.chalice, # Cherubs "debla": self.debla, "lorquiana": self.lorquiana, "zarabanda": self.zarabanda, "taranto": self.taranto, "verdiales": self.verdiales, "cante": self.cante, "cantina": self.cantina, "aubade": self.aubade, "tirana": self.tirana, "ruby": self.ruby, "tiento": self.tiento, # skip "anyPrayer" "pillar": self.pillar, # Stats # skip "healthLevel" # skip "fervourLevel" # skip "swordLevel" # Skills # skip "combo" # skip "charged" # skip "ranged" # skip "dive" # skip "lunge" "chargeBeam": self.charge_beam, "rangedAttack": self.ranged, # Main quest "holyWounds3": lambda state: self.holy_wounds(state, 3), "masks1": lambda state: self.masks(state, 1), "masks2": lambda state: self.masks(state, 2), "masks3": lambda state: self.masks(state, 3), "guiltBead": self.guilt_bead, # LOTL quest "cloth": self.cloth, "hand": self.hand, "hatchedEgg": self.hatched_egg, # Tirso quest "herbs1": lambda state: self.herbs(state, 1), "herbs2": lambda state: self.herbs(state, 2), "herbs3": lambda state: self.herbs(state, 3), "herbs4": lambda state: self.herbs(state, 4), "herbs5": lambda state: self.herbs(state, 5), "herbs6": lambda state: self.herbs(state, 6), # Tentudia quest "tentudiaRemains1": lambda state: self.tentudia_remains(state, 1), "tentudiaRemains2": lambda state: self.tentudia_remains(state, 2), "tentudiaRemains3": lambda state: self.tentudia_remains(state, 3), # Gemino quest "emptyThimble": self.empty_thimble, "fullThimble": self.full_thimble, "driedFlowers": self.dried_flowers, # Altasgracias quest "ceremonyItems3": lambda state: self.ceremony_items(state, 3), "egg": self.egg, # Redento quest # skip "limestones", not actually used # skip "knots", not actually used # Cleofas quest "marksOfRefuge3": lambda state: self.marks_of_refuge(state, 3), "cord": self.cord, # Crisanta quest "scapular": self.scapular, "trueHeart": self.true_heart, "traitorEyes2": lambda state: self.traitor_eyes(state, 2), # Jibrael quest "bell": self.bell, "verses4": lambda state: self.verses(state) >= 4, # Movement tech "canAirStall": self.can_air_stall, "canDawnJump": self.can_dawn_jump, "canWaterJump": self.can_water_jump, # Breakable tech "canBreakHoles": self.can_break_holes, "canDiveLaser": self.can_dive_laser, # Root tech "canWalkOnRoot": self.can_walk_on_root, "canClimbOnRoot": self.can_climb_on_root, # Lung tech "canSurvivePoison1": self.can_survive_poison_1, "canSurvivePoison2": self.can_survive_poison_2, "canSurvivePoison3": self.can_survive_poison_3, # Enemy tech "canEnemyBounce": _bool_rule(self.can_enemy_bounce), "canEnemyUpslash": self.can_enemy_upslash, # Reaching rooms "guiltRooms1": lambda state: self.guilt_rooms(state, 1), "guiltRooms2": lambda state: self.guilt_rooms(state, 2), "guiltRooms3": lambda state: self.guilt_rooms(state, 3), "guiltRooms4": lambda state: self.guilt_rooms(state, 4), "guiltRooms5": lambda state: self.guilt_rooms(state, 5), "guiltRooms6": lambda state: self.guilt_rooms(state, 6), "guiltRooms7": lambda state: self.guilt_rooms(state, 7), "swordRooms1": lambda state: self.sword_rooms(state, 1), "swordRooms2": lambda state: self.sword_rooms(state, 2), "swordRooms3": lambda state: self.sword_rooms(state, 3), "swordRooms4": lambda state: self.sword_rooms(state, 4), "swordRooms5": lambda state: self.sword_rooms(state, 5), "swordRooms6": lambda state: self.sword_rooms(state, 6), "swordRooms7": lambda state: self.sword_rooms(state, 7), "redentoRooms2": lambda state: self.redento_rooms(state, 2), "redentoRooms3": lambda state: self.redento_rooms(state, 3), "redentoRooms4": lambda state: self.redento_rooms(state, 4), "redentoRooms5": lambda state: self.redento_rooms(state, 5), "miriamRooms5": self.all_miriam_rooms, "amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1, "amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2, "amanecidaRooms3": lambda state: self.amanecida_rooms(state) >= 3, "amanecidaRooms4": lambda state: self.amanecida_rooms(state) >= 4, "chaliceRooms3": lambda state: self.chalice_rooms(state) >= 3, # Crossing gaps "canCrossGap1": self.can_cross_gap_1, "canCrossGap2": self.can_cross_gap_2, "canCrossGap3": self.can_cross_gap_3, "canCrossGap4": self.can_cross_gap_4, "canCrossGap5": self.can_cross_gap_5, "canCrossGap6": self.can_cross_gap_6, "canCrossGap7": self.can_cross_gap_7, "canCrossGap8": self.can_cross_gap_8, "canCrossGap9": self.can_cross_gap_9, "canCrossGap10": self.can_cross_gap_10, "canCrossGap11": self.can_cross_gap_11, # Events in different scenes "openedDCGateW": self.opened_dc_gate_w, "openedDCGateE": self.opened_dc_gate_e, "openedDCLadder": self.opened_dc_ladder, "openedWOTWCave": self.opened_wotw_cave, "rodeGotPElevator": self.rode_gotp_elevator, "openedConventLadder": self.opened_convent_ladder, "brokeJondoBellW": self.broke_jondo_bell_w, "brokeJondoBellE": self.broke_jondo_bell_e, "openedMoMLadder": self.opened_mom_ladder, "openedTSCGate": self.opened_tsc_gate, "openedARLadder": self.opened_ar_ladder, "brokeBotTCStatue": self.broke_bottc_statue, "openedWotHPGate": self.opened_wothp_gate, "openedBotSSLadder": self.opened_botss_ladder, # Special skips "upwarpSkipsAllowed": _bool_rule(self.upwarp_skips_allowed), "mourningSkipAllowed": _bool_rule(self.mourning_skip_allowed), "enemySkipsAllowed": _bool_rule(self.enemy_skips_allowed), "obscureSkipsAllowed": _bool_rule(self.obscure_skips_allowed), "preciseSkipsAllowed": _bool_rule(self.precise_skips_allowed), # Bosses "canBeatBrotherhoodBoss": self.can_beat_brotherhood_boss, "canBeatMercyBoss": self.can_beat_mercy_boss, "canBeatConventBoss": self.can_beat_convent_boss, "canBeatGrievanceBoss": self.can_beat_grievance_boss, "canBeatBridgeBoss": self.can_beat_bridge_boss, "canBeatMothersBoss": self.can_beat_mothers_boss, "canBeatCanvasesBoss": self.can_beat_canvases_boss, "canBeatPrisonBoss": self.can_beat_prison_boss, "canBeatRooftopsBoss": self.can_beat_rooftops_boss, "canBeatOssuaryBoss": self.can_beat_ossuary_boss, "canBeatMourningBoss": self.can_beat_mourning_boss, "canBeatGraveyardBoss": self.can_beat_graveyard_boss, "canBeatJondoBoss": self.can_beat_jondo_boss, "canBeatPatioBoss": self.can_beat_patio_boss, "canBeatWallBoss": self.can_beat_wall_boss, "canBeatHallBoss": self.can_beat_hall_boss, "canBeatPerpetua": self.can_beat_perpetua, "canBeatLegionary": self.can_beat_legionary } boss_strength_indirect_regions: List[str] = [ # flasks "D01Z05S05[SW]", "D02Z02S04[W]", "D03Z02S08[W]", "D03Z03S04[SW]", "D04Z02S13[W]", "D05Z01S08[NW]", "D20Z01S07[NE]", # quicksilver "D01Z05S01[W]" ] guilt_indirect_regions: List[str] = [ "D01Z04S01[NE]", "D02Z02S11[W]", "D03Z03S02[NE]", "D04Z02S02[SE]", "D05Z01S05[NE]", "D09Z01S05[W]", "D17Z01S04[W]" ] sword_indirect_regions: List[str] = [ "D01Z02S07[E]", "D01Z02S02[SW]", "D20Z01S04[E]", "D01Z05S23[W]", "D02Z03S02[NE]", "D04Z02S21[NE]", "D05Z01S21[NW]", "D06Z01S15[NE]", "D17Z01S07[SW]" ] redento_indirect_regions: List[str] = [ "D03Z01S04[E]", "D03Z02S10[N]", "D17Z01S05[S]", "D17BZ02S01[FrontR]", "D01Z03S04[E]", "D08Z01S01[W]", "D04Z01S03[E]", "D04Z02S01[W]", "D06Z01S18[-Cherubs]", "D04Z02S08[E]", "D04BZ02S01[Redento]", "D17Z01S07[NW]" ] miriam_indirect_regions: List[str] = [ "D02Z03S07[NWW]", "D03Z03S07[NW]", "D04Z04S01[E]", "D05Z01S06[W]", "D06Z01S17[E]" ] chalice_indirect_regions: List[str] = [ "D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]", "D05Z01S11[SE]", "D05Z02S02[NW]", "D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]" ] self.indirect_regions: Dict[str, List[str]] = { "openedDCGateW": ["D20Z01S04[E]", "D01Z05S23[W]"], "openedDCGateE": ["D01Z05S10[SE]", "D01Z04S09[W]"], "openedDCLadder": ["D01Z05S25[NE]", "D01Z05S02[S]"], "openedWOTWCave": ["D02Z01S01[SW]", "D02Z01S08[E]", "D02Z01S02[]"], "rodeGotPElevator": ["D02Z03S14[E]", "D02Z02S13[W]", "D02Z02S06[E]", "D02Z02S12[W]", "D02Z02S08[W]"], "openedConventLadder": ["D02Z03S02[N]", "D02Z03S15[E]", "D02Z03S19[E]", "D02Z03S10[W]", "D02Z03S22[W]"], "brokeJondoBellW": ["D03Z02S08[N]", "D03Z02S12[E]", "D03Z02S10[S]", "D03Z02S10[-Cherubs]"], "brokeJondoBellE": ["D03Z02S04[NE]", "D03Z02S11[W]", "D03Z02S03[E]"], "openedMoMLadder": ["D04Z02S11[E]", "D04Z02S09[W]", "D06Z01S23[S]", "D04Z02S04[N]"], "openedTSCGate": ["D05Z02S06[SE]", "D05Z01S21[-Cherubs]"], "openedARLadder": ["D06Z01S22[Sword]", "D06Z01S20[W]", "D04Z02S06[N]", "D06Z01S01[-Cherubs]"], "brokeBotTCStatue": ["D08Z03S03[W]", "D08Z02S03[W]"], "openedWotHPGate": ["D09Z01S13[E]", "D09Z01S03[W]", "D09Z01S08[W]"], "openedBotSSLadder": ["D17Z01S05[S]", "D17BZ02S01[FrontR]"], "canBeatBrotherhoodBoss": [*boss_strength_indirect_regions, "D17Z01S05[E]", "D17Z01S03[W]"], "canBeatMercyBoss": [*boss_strength_indirect_regions, "D01Z04S19[E]", "D01Z04S12[W]"], "canBeatConventBoss": [*boss_strength_indirect_regions, "D02Z03S09[E]", "D02Z03S21[W]"], "canBeatGrievanceBoss": [*boss_strength_indirect_regions, "D03Z03S11[E]", "D03Z03S16[W]"], "canBeatBridgeBoss": [*boss_strength_indirect_regions, "D01Z03S06[E]", "D08Z02S01[W]"], "canBeatMothersBoss": [*boss_strength_indirect_regions, "D04Z02S15[E]", "D04Z02S21[W]"], "canBeatCanvasesBoss": [*boss_strength_indirect_regions, "D05Z02S06[NE]", "D05Z01S21[SW]"], "canBeatPrisonBoss": [*boss_strength_indirect_regions, "D09Z01S05[SE]", "D09Z01S08[S]"], "canBeatRooftopsBoss": [*boss_strength_indirect_regions, "D06Z01S19[E]", "D07Z01S01[W]"], "canBeatOssuaryBoss": [*boss_strength_indirect_regions, "D01BZ06S01[E]"], "canBeatMourningBoss": [*boss_strength_indirect_regions, "D20Z02S07[W]"], "canBeatGraveyardBoss": [*boss_strength_indirect_regions, "D01Z06S01[Santos]", "D02Z03S18[NW]", "D02Z02S03[NE]"], "canBeatJondoBoss": [*boss_strength_indirect_regions, "D01Z06S01[Santos]", "D20Z01S06[NE]", "D20Z01S04[W]", "D03Z01S04[E]", "D03Z02S10[N]"], "canBeatPatioBoss": [*boss_strength_indirect_regions, "D01Z06S01[Santos]", "D06Z01S02[W]", "D04Z01S03[E]", "D04Z01S01[W]", "D06Z01S18[-Cherubs]"], "canBeatWallBoss": [*boss_strength_indirect_regions, "D01Z06S01[Santos]", "D09Z01S09[Cell24]", "D09Z01S11[E]", "D06Z01S13[W]"], "canBeatHallBoss": [*boss_strength_indirect_regions, "D08Z01S02[NE]", "D08Z03S02[NW]"], "canBeatPerpetua": boss_strength_indirect_regions, "canBeatLegionary": boss_strength_indirect_regions, "guiltRooms1": guilt_indirect_regions, "guiltRooms2": guilt_indirect_regions, "guiltRooms3": guilt_indirect_regions, "guiltRooms4": guilt_indirect_regions, "guiltRooms5": guilt_indirect_regions, "guiltRooms6": guilt_indirect_regions, "guiltRooms7": guilt_indirect_regions, "swordRooms1": sword_indirect_regions, "swordRooms2": sword_indirect_regions, "swordRooms3": sword_indirect_regions, "swordRooms4": sword_indirect_regions, "swordRooms5": sword_indirect_regions, "swordRooms6": sword_indirect_regions, "swordRooms7": sword_indirect_regions, "redentoRooms2": redento_indirect_regions, "redentoRooms3": redento_indirect_regions, "redentoRooms4": redento_indirect_regions, "redentoRooms5": redento_indirect_regions, "miriamRooms5": miriam_indirect_regions, "chaliceRooms3": chalice_indirect_regions } self.indirect_regions["amanecidaRooms1"] = [*self.indirect_regions["canBeatGraveyardBoss"], *self.indirect_regions["canBeatJondoBoss"], *self.indirect_regions["canBeatPatioBoss"], *self.indirect_regions["canBeatWallBoss"]] self.indirect_regions["amanecidaRooms2"] = [*self.indirect_regions["canBeatGraveyardBoss"], *self.indirect_regions["canBeatJondoBoss"], *self.indirect_regions["canBeatPatioBoss"], *self.indirect_regions["canBeatWallBoss"]] self.indirect_regions["amanecidaRooms3"] = [*self.indirect_regions["canBeatGraveyardBoss"], *self.indirect_regions["canBeatJondoBoss"], *self.indirect_regions["canBeatPatioBoss"], *self.indirect_regions["canBeatWallBoss"]] self.indirect_regions["amanecidaRooms4"] = [*self.indirect_regions["canBeatGraveyardBoss"], *self.indirect_regions["canBeatJondoBoss"], *self.indirect_regions["canBeatPatioBoss"], *self.indirect_regions["canBeatWallBoss"]] def req_is_region(self, string: str) -> bool: return (string[0] == "D" and string[3] == "Z" and string[6] == "S")\ or (string[0] == "D" and string[3] == "B" and string[4] == "Z" and string[7] == "S") def load_rule(self, obj_is_region: bool, name: str, obj: Dict[str, Any]) -> Callable[[CollectionState], bool]: clauses = [] clauses_are_impossible_if_empty = False rule_indirect_conditions = [] for clause in obj["logic"]: reqs = [] clause_indirect_conditions = [] clause_is_impossible = False for req in clause["item_requirements"]: if self.req_is_region(req): if obj_is_region: # add to indirect conditions if object and requirement are doors clause_indirect_conditions.append((req, f"{name} -> {obj['target']}")) reqs.append(lambda state, req=req: state.can_reach_region(req, self.player)) else: string_rule = self.string_rules[req] if string_rule is _never: # This clause is not possible with the options this player has chosen. clause_is_impossible = True break elif string_rule is _always: # Don't need to add a rule that is always True with the options this player has chosen. # Continue to the next requirement. continue if obj_is_region and req in self.indirect_regions: # add to indirect conditions if object is door and requirement has list of regions for region in self.indirect_regions[req]: clause_indirect_conditions.append((region, f"{name} -> {obj['target']}")) reqs.append(self.string_rules[req]) if clause_is_impossible: # At least one clause was impossible, so if all clauses were impossible, the entire rule is impossible. clauses_are_impossible_if_empty = True # Continue to the next clause. continue rule_indirect_conditions.extend(clause_indirect_conditions) # Combine the requirements if there are multiple. # Requirements are AND-ed together. if len(reqs) == 1: clauses.append(reqs[0]) else: def req_func(state, reqs=reqs): for req in reqs: if not req(state): return False return True clauses.append(req_func) # Combine the clauses if there are multiple. # Clauses are OR-ed together. if not clauses: # There is no need to register the indirect conditions if it turns out the rule is impossible or always # possible. rule_indirect_conditions.clear() if clauses_are_impossible_if_empty: to_return = _never else: to_return = _always elif len(clauses) == 1: to_return = clauses[0] else: def clause_func(state, clauses=clauses): for clause in clauses: if clause(state): return True return False to_return = clause_func # Update the list of indirect conditions to add. self.indirect_conditions.extend(rule_indirect_conditions) return to_return # Relics def blood(self, state: CollectionState) -> bool: return state.has("Blood Perpetuated in Sand", self.player) def root(self, state: CollectionState) -> bool: return state.has("Three Gnarled Tongues", self.player) def linen(self, state: CollectionState) -> bool: return state.has("Linen of Golden Thread", self.player) def nail(self, state: CollectionState) -> bool: return state.has("Nail Uprooted from Dirt", self.player) def shroud(self, state: CollectionState) -> bool: return state.has("Shroud of Dreamt Sins", self.player) def lung(self, state: CollectionState) -> bool: return state.has("Silvered Lung of Dolphos", self.player) # Keys def bronze_key(self, state: CollectionState) -> bool: return state.has("Key of the Secular", self.player) def silver_key(self, state: CollectionState) -> bool: return state.has("Key of the Scribe", self.player) def gold_key(self, state: CollectionState) -> bool: return state.has("Key of the Inquisitor", self.player) def peaks_key(self, state: CollectionState) -> bool: return state.has("Key of the High Peaks", self.player) def elder_key(self, state: CollectionState) -> bool: return state.has("Key to the Chamber of the Eldest Brother", self.player) def wood_key(self, state: CollectionState) -> bool: return state.has("Key Grown from Twisted Wood", self.player) # Collections def cherubs(self, state: CollectionState) -> int: return state.count("Child of Moonlight", self.player) def bones(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "bones" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("bones", self.player, count) # def tears(): # Special items def dash(self, state: CollectionState) -> bool: return state.has("Dash Ability", self.player) def wall_climb(self, state: CollectionState) -> bool: return state.has("Wall Climb Ability", self.player) #def air_impulse(): def boots(self, state: CollectionState) -> bool: return state.has("Boots of Pleading", self.player) def double_jump(self, state: CollectionState) -> bool: return state.has("Purified Hand of the Nun", self.player) # Speed boosts def wheel(self, state: CollectionState) -> bool: return state.has("The Young Mason's Wheel", self.player) def dawn_heart(self, state: CollectionState) -> bool: return state.has("Brilliant Heart of Dawn", self.player) # Health boosts def flasks(self, state: CollectionState) -> int: doors = ( "D01Z05S05[SW]", "D02Z02S04[W]", "D03Z02S08[W]", "D03Z03S04[SW]", "D04Z02S13[W]", "D05Z01S08[NW]", "D20Z01S07[NE]" ) for door in doors: if state.can_reach_region(door, self.player): return state.count("Empty Bile Vessel", self.player) return 0 def quicksilver(self, state: CollectionState) -> int: return state.count("Quicksilver", self.player) if state.can_reach_region("D01Z05S01[W]", self.player) else 0 # Puzzles def red_wax(self, state: CollectionState) -> int: return state.count("Bead of Red Wax", self.player) def blue_wax(self, state: CollectionState) -> int: return state.count("Bead of Blue Wax", self.player) def chalice(self, state: CollectionState) -> bool: return state.has("Chalice of Inverted Verses", self.player) # Cherubs def debla(self, state: CollectionState) -> bool: return state.has("Debla of the Lights", self.player) def lorquiana(self, state: CollectionState) -> bool: return state.has("Lorquiana", self.player) def zarabanda(self, state: CollectionState) -> bool: return state.has("Zarabanda of the Safe Haven", self.player) def taranto(self, state: CollectionState) -> bool: return state.has("Taranto to my Sister", self.player) def verdiales(self, state: CollectionState) -> bool: return state.has("Verdiales of the Forsaken Hamlet", self.player) def cante(self, state: CollectionState) -> bool: return state.has("Cante Jondo of the Three Sisters", self.player) def cantina(self, state: CollectionState) -> bool: return state.has("Cantina of the Blue Rose", self.player) def aubade(self, state: CollectionState) -> bool: return ( state.has("Aubade of the Nameless Guardian", self.player) and self.total_fervour(state) >= 90 ) def tirana(self, state: CollectionState) -> bool: return ( state.has("Tirana of the Celestial Bastion", self.player) and self.total_fervour(state) >= 90 ) def ruby(self, state: CollectionState) -> bool: return state.has("Cloistered Ruby", self.player) def tiento(self, state: CollectionState) -> bool: return state.has("Tiento to my Sister", self.player) def any_small_prayer(self, state: CollectionState) -> bool: return ( self.debla(state) or self.lorquiana(state) or self.zarabanda(state) or self.taranto(state) or self.verdiales(state) or self.cante(state) or self.cantina(state) or self.tiento(state) or state.has_any(( "Campanillero to the Sons of the Aurora", "Mirabras of the Return to Port", "Romance to the Crimson Mist", "Saeta Dolorosa", "Seguiriya to your Eyes like Stars", "Verdiales of the Forsaken Hamlet", "Zambra to the Resplendent Crown" ), self.player) ) def pillar(self, state: CollectionState) -> bool: return ( self.debla(state) or self.taranto(state) or self.ruby(state) ) def can_use_any_prayer(self, state: CollectionState) -> bool: return ( self.any_small_prayer(state) or self.tirana(state) or self.aubade(state) ) # Stats def total_fervour(self, state: CollectionState) -> int: return ( 60 + (20 * min(6, state.count("Fervour Upgrade", self.player))) + (10 * min(3, state.count("Bead of Blue Wax", self.player))) ) # Skills def combo(self, state: CollectionState) -> int: return state.count("Combo Skill", self.player) def charged(self, state: CollectionState) -> int: return state.count("Charged Skill", self.player) def ranged(self, state: CollectionState) -> bool: return state.has("Ranged Skill", self.player) def dive(self, state: CollectionState) -> int: return state.count("Dive Skill", self.player) def lunge(self, state: CollectionState) -> int: return state.count("Lunge Skill", self.player) def charge_beam(self, state: CollectionState) -> bool: return self.charged(state) >= 3 # Main quest def holy_wounds(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "wounds" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("wounds", self.player, count) def masks(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "masks" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("masks", self.player, count) def guilt_bead(self, state: CollectionState) -> bool: return state.has("Weight of True Guilt", self.player) # LOTL quest def cloth(self, state: CollectionState) -> bool: return state.has("Linen Cloth", self.player) def hand(self, state: CollectionState) -> bool: return state.has("Severed Hand", self.player) def hatched_egg(self, state: CollectionState) -> bool: return state.has("Hatched Egg of Deformity", self.player) # Tirso quest def herbs(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "tirso" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("tirso", self.player, count) # Tentudia quest def tentudia_remains(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "tentudia" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("tentudia", self.player, count) # Gemino quest def empty_thimble(self, state: CollectionState) -> bool: return state.has("Empty Golden Thimble", self.player) def full_thimble(self, state: CollectionState) -> bool: return state.has("Golden Thimble Filled with Burning Oil", self.player) def dried_flowers(self, state: CollectionState) -> bool: return state.has("Dried Flowers bathed in Tears", self.player) # Altasgracias quest def ceremony_items(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "egg" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("egg", self.player, count) def egg(self, state: CollectionState) -> bool: return state.has("Egg of Deformity", self.player) # Redento quest def limestones(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "toe" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("toe", self.player, count) def knots(self, state: CollectionState) -> int: return state.count("Knot of Rosary Rope", self.player) if state.can_reach_region("D17Z01S07[NW]", self.player)\ else 0 # Cleofas quest def marks_of_refuge(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "marks" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("marks", self.player, count) def cord(self, state: CollectionState) -> bool: return state.has("Cord of the True Burying", self.player) # Crisanta quest def scapular(self, state: CollectionState) -> bool: return state.has("Incomplete Scapular", self.player) def true_heart(self, state: CollectionState) -> bool: return state.has("Apodictic Heart of Mea Culpa", self.player) def traitor_eyes(self, state: CollectionState, count: int) -> bool: # Count of unique items in the "eye" item group that have been collected into state. # BlasphemousWorld.collect/remove adjust the count when items in the group are collected/removed. return state.has("eye", self.player, count) # Jibrael quest def bell(self, state: CollectionState) -> bool: return state.has("Petrified Bell", self.player) def verses(self, state: CollectionState) -> int: return state.count("Verses Spun from Gold", self.player) # Movement tech def can_water_jump(self, state: CollectionState) -> bool: return ( self.nail(state) or self.double_jump(state) ) # Breakable tech def can_break_holes(self, state: CollectionState) -> bool: return ( self.charged(state) > 0 or self.dive(state) > 0 or self.lunge(state) >= 3 and self.dash(state) or self.can_use_any_prayer(state) ) # Root tech def can_walk_on_root(self, state: CollectionState) -> bool: return self.root(state) def can_climb_on_root(self, state: CollectionState) -> bool: return ( self.root(state) and self.wall_climb(state) ) # Crossing gaps def can_cross_gap_1(self, state: CollectionState) -> bool: return ( self.double_jump(state) or self.can_dawn_jump(state) or self.wheel(state) or self.can_air_stall(state) ) def can_cross_gap_2(self, state: CollectionState) -> bool: return ( self.double_jump(state) or self.can_dawn_jump(state) or self.wheel(state) ) def can_cross_gap_3(self, state: CollectionState) -> bool: return ( self.double_jump(state) or self.can_dawn_jump(state) or self.wheel(state) and self.can_air_stall(state) ) def can_cross_gap_4(self, state: CollectionState) -> bool: return ( self.double_jump(state) or self.can_dawn_jump(state) ) def can_cross_gap_5(self, state: CollectionState) -> bool: return ( self.double_jump(state) or self.can_dawn_jump(state) and self.can_air_stall(state) ) def can_cross_gap_6(self, state: CollectionState) -> bool: return self.double_jump(state) def can_cross_gap_7(self, state: CollectionState) -> bool: return ( self.double_jump(state) and ( self.can_dawn_jump(state) or self.wheel(state) or self.can_air_stall(state) ) ) def can_cross_gap_8(self, state: CollectionState) -> bool: return ( self.double_jump(state) and ( self.can_dawn_jump(state) or self.wheel(state) ) ) def can_cross_gap_9(self, state: CollectionState) -> bool: return ( self.double_jump(state) and ( self.can_dawn_jump(state) or self.wheel(state) and self.can_air_stall(state) ) ) def can_cross_gap_10(self, state: CollectionState) -> bool: return ( self.double_jump(state) and self.can_dawn_jump(state) ) def can_cross_gap_11(self, state: CollectionState) -> bool: return ( self.double_jump(state) and self.can_dawn_jump(state) and self.can_air_stall(state) ) # Events that trigger in different scenes def opened_dc_gate_w(self, state: CollectionState) -> bool: return ( state.can_reach_region("D20Z01S04[E]", self.player) or state.can_reach_region("D01Z05S23[W]", self.player) ) def opened_dc_gate_e(self, state: CollectionState) -> bool: return ( state.can_reach_region("D01Z05S10[SE]", self.player) or state.can_reach_region("D01Z04S09[W]", self.player) ) def opened_dc_ladder(self, state: CollectionState) -> bool: return ( state.can_reach_region("D01Z05S25[NE]", self.player) or state.can_reach_region("D01Z05S02[S]", self.player) ) def opened_wotw_cave(self, state: CollectionState) -> bool: return ( state.can_reach_region("D02Z01S01[SW]", self.player) or self.wall_climb(state) and state.can_reach_region("D02Z01S08[E]", self.player) or state.can_reach_region("D02Z01S02[]", self.player) ) def rode_gotp_elevator(self, state: CollectionState) -> bool: return ( state.can_reach_region("D02Z03S14[E]", self.player) or state.can_reach_region("D02Z02S13[W]", self.player) or state.can_reach_region("D02Z02S06[E]", self.player) or state.can_reach_region("D02Z02S12[W]", self.player) or state.can_reach_region("D02Z02S08[W]", self.player) ) def opened_convent_ladder(self, state: CollectionState) -> bool: return ( state.can_reach_region("D02Z03S02[N]", self.player) or state.can_reach_region("D02Z03S15[E]", self.player) or state.can_reach_region("D02Z03S19[E]", self.player) or state.can_reach_region("D02Z03S10[W]", self.player) or state.can_reach_region("D02Z03S22[W]", self.player) ) def broke_jondo_bell_w(self, state: CollectionState) -> bool: return ( state.can_reach_region("D03Z02S08[N]", self.player) or state.can_reach_region("D03Z02S12[E]", self.player) and self.dash(state) or state.can_reach_region("D03Z02S10[S]", self.player) or state.can_reach_region("D03Z02S10[-Cherubs]", self.player) ) def broke_jondo_bell_e(self, state: CollectionState) -> bool: return ( state.can_reach_region("D03Z02S04[NE]", self.player) or state.can_reach_region("D03Z02S11[W]", self.player) or state.can_reach_region("D03Z02S03[E]", self.player) and ( self.can_cross_gap_5(state) or self.can_enemy_bounce and self.can_cross_gap_3(state) ) ) def opened_mom_ladder(self, state: CollectionState) -> bool: return ( state.can_reach_region("D04Z02S11[E]", self.player) or state.can_reach_region("D04Z02S09[W]", self.player) or state.can_reach_region("D06Z01S23[S]", self.player) or state.can_reach_region("D04Z02S04[N]", self.player) ) def opened_tsc_gate(self, state: CollectionState) -> bool: return ( state.can_reach_region("D05Z02S06[SE]", self.player) or state.can_reach_region("D05Z01S21[-Cherubs]", self.player) ) def opened_ar_ladder(self, state: CollectionState) -> bool: return ( state.can_reach_region("D06Z01S22[Sword]", self.player) or state.can_reach_region("D06Z01S20[W]", self.player) or state.can_reach_region("D04Z02S06[N]", self.player) or state.can_reach_region("D06Z01S01[-Cherubs]", self.player) ) def broke_bottc_statue(self, state: CollectionState) -> bool: return ( state.can_reach_region("D08Z03S03[W]", self.player) or state.can_reach_region("D08Z02S03[W]", self.player) ) def opened_wothp_gate(self, state: CollectionState) -> bool: return ( state.can_reach_region("D09Z01S13[E]", self.player) or state.can_reach_region("D09Z01S03[W]", self.player) or state.can_reach_region("D09Z01S08[W]", self.player) ) def opened_botss_ladder(self, state: CollectionState) -> bool: return ( state.can_reach_region("D17Z01S05[S]", self.player) or state.can_reach_region("D17BZ02S01[FrontR]", self.player) ) # Bosses def can_beat_brotherhood_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "warden") and ( state.can_reach_region("D17Z01S05[E]", self.player) or state.can_reach_region("D17Z01S03[W]", self.player) ) ) def can_beat_mercy_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "ten-piedad") and ( state.can_reach_region("D01Z04S19[E]", self.player) or state.can_reach_region("D01Z04S12[W]", self.player) ) ) def can_beat_convent_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "charred-visage") and ( state.can_reach_region("D02Z03S09[E]", self.player) or state.can_reach_region("D02Z03S21[W]", self.player) ) ) def can_beat_grievance_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "tres-angustias") and ( self.wall_climb(state) or self.double_jump(state) ) and ( state.can_reach_region("D03Z03S11[E]", self.player) or state.can_reach_region("D03Z03S16[W]", self.player) ) ) def can_beat_bridge_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "esdras") and ( state.can_reach_region("D01Z03S06[E]", self.player) or state.can_reach_region("D08Z02S01[W]", self.player) ) ) def can_beat_mothers_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "melquiades") and ( state.can_reach_region("D04Z02S15[E]", self.player) or state.can_reach_region("D04Z02S21[W]", self.player) ) ) def can_beat_canvases_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "exposito") and ( state.can_reach_region("D05Z02S06[NE]", self.player) or state.can_reach_region("D05Z01S21[SW]", self.player) ) ) def can_beat_prison_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "quirce") and ( state.can_reach_region("D09Z01S05[SE]", self.player) or state.can_reach_region("D09Z01S08[S]", self.player) ) ) def can_beat_rooftops_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "crisanta") and ( state.can_reach_region("D06Z01S19[E]", self.player) or state.can_reach_region("D07Z01S01[W]", self.player) ) ) def can_beat_ossuary_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "isidora") and state.can_reach_region("D01BZ06S01[E]", self.player) ) def can_beat_mourning_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "sierpes") and state.can_reach_region("D20Z02S07[W]", self.player) ) def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( self.has_boss_strength(state, "amanecida", player_strength) and self.wall_climb(state) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D02Z03S18[NW]", self.player) and state.can_reach_region("D02Z02S03[NE]", self.player) ) def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and ( state.can_reach_region("D20Z01S06[NE]", self.player) or state.can_reach_region("D20Z01S04[W]", self.player) ) and ( state.can_reach_region("D03Z01S04[E]", self.player) or state.can_reach_region("D03Z02S10[N]", self.player) ) ) def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D06Z01S02[W]", self.player) and ( state.can_reach_region("D04Z01S03[E]", self.player) or state.can_reach_region("D04Z01S01[W]", self.player) or state.can_reach_region("D06Z01S18[-Cherubs]", self.player) ) ) def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool: return ( self.has_boss_strength(state, "amanecida", player_strength) and state.can_reach_region("D01Z06S01[Santos]", self.player) and state.can_reach_region("D09Z01S09[Cell24]", self.player) and ( state.can_reach_region("D09Z01S11[E]", self.player) or state.can_reach_region("D06Z01S13[W]", self.player) ) ) def can_beat_hall_boss(self, state: CollectionState) -> bool: return ( self.has_boss_strength(state, "laudes") and ( state.can_reach_region("D08Z01S02[NE]", self.player) or state.can_reach_region("D08Z03S02[NW]", self.player) ) ) def can_beat_perpetua(self, state: CollectionState) -> bool: return self.has_boss_strength(state, "perpetua") def can_beat_legionary(self, state: CollectionState) -> bool: return self.has_boss_strength(state, "legionary") def get_player_strength(self, state: CollectionState) -> float: life: int = state.count("Life Upgrade", self.player) sword: int = state.count("Mea Culpa Upgrade", self.player) fervour: int = state.count("Fervour Upgrade", self.player) flasks: int = self.flasks(state) quicksilver: int = self.quicksilver(state) player_strength: float = ( min(6, life) * 0.25 / 6 + min(7, sword) * 0.25 / 7 + min(6, fervour) * 0.20 / 6 + min(8, flasks) * 0.15 / 8 + min(5, quicksilver) * 0.15 / 5 ) return player_strength def has_boss_strength(self, state: CollectionState, boss: str, player_strength: float | None = None) -> bool: if player_strength is None: return self.get_player_strength(state) >= self.boss_strengths[boss] else: return player_strength >= self.boss_strengths[boss] def guilt_rooms(self, state: CollectionState, count: int) -> bool: doors = ( "D01Z04S01[NE]", "D02Z02S11[W]", "D03Z03S02[NE]", "D04Z02S02[SE]", "D05Z01S05[NE]", "D09Z01S05[W]", "D17Z01S04[W]", ) total: int = 0 for door in doors: total += state.can_reach_region(door, self.player) if total >= count: return True return False def sword_rooms(self, state: CollectionState, count: int) -> bool: doors = ( ("D01Z02S07[E]", "D01Z02S02[SW]"), ("D20Z01S04[E]", "D01Z05S23[W]"), ("D02Z03S02[NE]",), ("D04Z02S21[NE]",), ("D05Z01S21[NW]",), ("D06Z01S15[NE]",), ("D17Z01S07[SW]",) ) total: int = 0 for subdoors in doors: for door in subdoors: if state.can_reach_region(door, self.player): total += 1 break if total >= count: return True return False def redento_rooms(self, state: CollectionState, count: int) -> bool: if not ( state.can_reach_region("D03Z01S04[E]", self.player) or state.can_reach_region("D03Z02S10[N]", self.player) ): # Realistically, count should never be zero or negative. return count < 1 if count == 1: return True if not ( state.can_reach_region("D17Z01S05[S]", self.player) or state.can_reach_region("D17BZ02S01[FrontR]", self.player) ): return False if count == 2: return True if not (state.can_reach_region("D01Z03S04[E]", self.player) or state.can_reach_region("D08Z01S01[W]", self.player)): return False if count == 3: return True if not (state.can_reach_region("D04Z01S03[E]", self.player) or state.can_reach_region("D04Z02S01[W]", self.player) or state.can_reach_region("D06Z01S18[-Cherubs]", self.player)): return False if count == 4: return True if not ( self.knots(state) >= 1 and self.limestones(state, 3) and (state.can_reach_region("D04Z02S08[E]", self.player) or state.can_reach_region("D04BZ02S01[Redento]", self.player)) ): return False return count == 5 def all_miriam_rooms(self, state: CollectionState) -> bool: doors = ( "D02Z03S07[NWW]", "D03Z03S07[NW]", "D04Z04S01[E]", "D05Z01S06[W]", "D06Z01S17[E]" ) for door in doors: if not state.can_reach_region(door, self.player): return False return True def amanecida_rooms(self, state: CollectionState) -> int: player_strength = self.get_player_strength(state) total: int = 0 if self.can_beat_graveyard_boss(state, player_strength): total += 1 if self.can_beat_jondo_boss(state, player_strength): total += 1 if self.can_beat_patio_boss(state, player_strength): total += 1 if self.can_beat_wall_boss(state, player_strength): total += 1 return total def chalice_rooms(self, state: CollectionState) -> int: doors = ( ("D03Z01S02[E]", "D01Z05S02[W]", "D20Z01S03[N]"), ("D05Z01S11[SE]", "D05Z02S02[NW]"), ("D09Z01S09[E]", "D09Z01S10[W]", "D09Z01S08[SE]", "D09Z01S02[SW]") ) total: int = 0 for subdoors in doors: for door in subdoors: if state.can_reach_region(door, self.player): total += 1 break return total