Blasphemous: Increase logic performance (#4585)

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Mysteryem
2026-05-09 15:50:34 +01:00
committed by GitHub
parent 2bd572c23d
commit 29a6f40c2b
3 changed files with 403 additions and 264 deletions
+3
View File
@@ -739,6 +739,9 @@ group_table: Dict[str, Set[str]] = {
"Broken Left Eye of the Traitor"}
}
# Because each item is only in a single group, a reverse lookup table from each item to its group can be created.
group_table_reverse: Dict[str, str] = {item: group for group, items in group_table.items() for item in items}
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
+377 -262
View File
@@ -1,5 +1,6 @@
from typing import Dict, List, Tuple, Any, Callable, TYPE_CHECKING
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
@@ -7,30 +8,145 @@ 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 = {
self.string_rules: dict[str, CollectionRule] = {
# Visibility flags
"DoubleJump": lambda state: bool(self.world.options.purified_hand),
"NormalLogic": lambda state: self.world.options.difficulty >= 1,
"NormalLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 1 \
and bool(self.world.options.purified_hand),
"HardLogic": lambda state: self.world.options.difficulty >= 2,
"HardLogicAndDoubleJump": lambda state: self.world.options.difficulty >= 2 \
and bool(self.world.options.purified_hand),
"EnemySkips": self.enemy_skips_allowed,
"EnemySkipsAndDoubleJump": lambda state: self.enemy_skips_allowed(state) \
and bool(self.world.options.purified_hand),
"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,
@@ -52,20 +168,20 @@ class BlasRules:
"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,
"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": lambda state: True,
"tears0": _always,
# Special items
"dash": self.dash,
@@ -118,13 +234,13 @@ class BlasRules:
# skip "dive"
# skip "lunge"
"chargeBeam": self.charge_beam,
"rangedAttack": lambda state: self.ranged(state) > 0,
"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,
"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
@@ -133,17 +249,17 @@ class BlasRules:
"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,
"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,
"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,
@@ -151,7 +267,7 @@ class BlasRules:
"driedFlowers": self.dried_flowers,
# Altasgracias quest
"ceremonyItems3": lambda state: self.ceremony_items(state) >= 3,
"ceremonyItems3": lambda state: self.ceremony_items(state, 3),
"egg": self.egg,
# Redento quest
@@ -159,13 +275,13 @@ class BlasRules:
# skip "knots", not actually used
# Cleofas quest
"marksOfRefuge3": lambda state: self.marks_of_refuge(state) >= 3,
"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,
"traitorEyes2": lambda state: self.traitor_eyes(state, 2),
# Jibrael quest
"bell": self.bell,
@@ -190,32 +306,32 @@ class BlasRules:
"canSurvivePoison3": self.can_survive_poison_3,
# Enemy tech
"canEnemyBounce": self.can_enemy_bounce,
"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,
"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,
"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,
"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": lambda state: self.miriam_rooms(state) >= 5,
"miriamRooms5": self.all_miriam_rooms,
"amanecidaRooms1": lambda state: self.amanecida_rooms(state) >= 1,
"amanecidaRooms2": lambda state: self.amanecida_rooms(state) >= 2,
@@ -254,11 +370,11 @@ class BlasRules:
"openedBotSSLadder": self.opened_botss_ladder,
# Special skips
"upwarpSkipsAllowed": self.upwarp_skips_allowed,
"mourningSkipAllowed": self.mourning_skip_allowed,
"enemySkipsAllowed": self.enemy_skips_allowed,
"obscureSkipsAllowed": self.obscure_skips_allowed,
"preciseSkipsAllowed": self.precise_skips_allowed,
"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,
@@ -498,30 +614,74 @@ class BlasRules:
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
self.indirect_conditions.append((req, f"{name} -> {obj['target']}"))
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]:
self.indirect_conditions.append((region, f"{name} -> {obj['target']}"))
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:
clauses.append(lambda state, reqs=reqs: all(req(state) for req in reqs))
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:
return lambda state: True
# 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:
return clauses[0]
to_return = clauses[0]
else:
return lambda state: any(clause(state) for clause in clauses)
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:
@@ -565,8 +725,10 @@ class BlasRules:
def cherubs(self, state: CollectionState) -> int:
return state.count("Child of Moonlight", self.player)
def bones(self, state: CollectionState) -> int:
return state.count_group_unique("bones", 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():
@@ -594,7 +756,7 @@ class BlasRules:
# Health boosts
def flasks(self, state: CollectionState) -> int:
doors = {
doors = (
"D01Z05S05[SW]",
"D02Z02S04[W]",
"D03Z02S08[W]",
@@ -602,10 +764,11 @@ class BlasRules:
"D04Z02S13[W]",
"D05Z01S08[NW]",
"D20Z01S07[NE]"
}
return state.count("Empty Bile Vessel", self.player) \
if sum(state.can_reach_region(door, self.player) for door in doors) >= 1 else 0
)
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
@@ -613,7 +776,7 @@ class BlasRules:
# 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)
@@ -670,7 +833,7 @@ class BlasRules:
or self.cante(state)
or self.cantina(state)
or self.tiento(state)
or state.has_any({
or state.has_any((
"Campanillero to the Sons of the Aurora",
"Mirabras of the Return to Port",
"Romance to the Crimson Mist",
@@ -678,7 +841,7 @@ class BlasRules:
"Seguiriya to your Eyes like Stars",
"Verdiales of the Forsaken Hamlet",
"Zambra to the Resplendent Crown"
}, self.player)
), self.player)
)
def pillar(self, state: CollectionState) -> bool:
@@ -710,8 +873,8 @@ class BlasRules:
def charged(self, state: CollectionState) -> int:
return state.count("Charged Skill", self.player)
def ranged(self, state: CollectionState) -> int:
return state.count("Ranged 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)
@@ -723,11 +886,15 @@ class BlasRules:
return self.charged(state) >= 3
# Main quest
def holy_wounds(self, state: CollectionState) -> int:
return state.count_group_unique("wounds", self.player)
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) -> int:
return state.count_group_unique("masks", self.player)
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)
@@ -743,12 +910,16 @@ class BlasRules:
return state.has("Hatched Egg of Deformity", self.player)
# Tirso quest
def herbs(self, state: CollectionState) -> int:
return state.count_group_unique("tirso", self.player)
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) -> int:
return state.count_group_unique("tentudia", self.player)
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:
@@ -761,23 +932,29 @@ class BlasRules:
return state.has("Dried Flowers bathed in Tears", self.player)
# Altasgracias quest
def ceremony_items(self, state: CollectionState) -> int:
return state.count_group_unique("egg", self.player)
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) -> int:
return state.count_group_unique("toe", self.player)
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) -> int:
return state.count_group_unique("marks", self.player)
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)
@@ -789,8 +966,10 @@ class BlasRules:
def true_heart(self, state: CollectionState) -> bool:
return state.has("Apodictic Heart of Mea Culpa", self.player)
def traitor_eyes(self, state: CollectionState) -> int:
return state.count_group_unique("eye", 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:
@@ -800,19 +979,6 @@ class BlasRules:
return state.count("Verses Spun from Gold", self.player)
# Movement tech
def can_air_stall(self, state: CollectionState) -> bool:
return (
self.ranged(state) > 0
and self.world.options.difficulty >= 1
)
def can_dawn_jump(self, state: CollectionState) -> bool:
return (
self.dawn_heart(state)
and self.dash(state)
and self.world.options.difficulty >= 1
)
def can_water_jump(self, state: CollectionState) -> bool:
return (
self.nail(state)
@@ -828,12 +994,6 @@ class BlasRules:
or self.can_use_any_prayer(state)
)
def can_dive_laser(self, state: CollectionState) -> bool:
return (
self.dive(state) >= 3
and self.world.options.difficulty >= 2
)
# Root tech
def can_walk_on_root(self, state: CollectionState) -> bool:
return self.root(state)
@@ -844,40 +1004,6 @@ class BlasRules:
and self.wall_climb(state)
)
# Lung tech
def can_survive_poison_1(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
or self.world.options.difficulty >= 2
)
def can_survive_poison_2(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 1
and self.tiento(state)
)
def can_survive_poison_3(self, state: CollectionState) -> bool:
return (
self.lung(state)
or self.world.options.difficulty >= 2
and self.tiento(state)
and self.total_fervour(state) >= 120
)
# Enemy tech
def can_enemy_bounce(self, state: CollectionState) -> bool:
return self.enemy_skips_allowed(state)
def can_enemy_upslash(self, state: CollectionState) -> bool:
return (
self.combo(state) >= 2
and self.enemy_skips_allowed(state)
)
# Crossing gaps
def can_cross_gap_1(self, state: CollectionState) -> bool:
return (
@@ -1021,7 +1147,7 @@ class BlasRules:
or state.can_reach_region("D03Z02S03[E]", self.player)
and (
self.can_cross_gap_5(state)
or self.can_enemy_bounce(state)
or self.can_enemy_bounce
and self.can_cross_gap_3(state)
)
)
@@ -1067,25 +1193,6 @@ class BlasRules:
or state.can_reach_region("D17BZ02S01[FrontR]", self.player)
)
# Special skips
def upwarp_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def mourning_skip_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def enemy_skips_allowed(self, state: CollectionState) -> bool:
return (
self.world.options.difficulty >= 2
and not self.world.options.enemy_randomizer
)
def obscure_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
def precise_skips_allowed(self, state: CollectionState) -> bool:
return self.world.options.difficulty >= 2
# Bosses
def can_beat_brotherhood_boss(self, state: CollectionState) -> bool:
return (
@@ -1183,18 +1290,18 @@ class BlasRules:
and state.can_reach_region("D20Z02S07[W]", self.player)
)
def can_beat_graveyard_boss(self, state: CollectionState) -> bool:
def can_beat_graveyard_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
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) -> bool:
def can_beat_jondo_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
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)
@@ -1206,9 +1313,9 @@ class BlasRules:
)
)
def can_beat_patio_boss(self, state: CollectionState) -> bool:
def can_beat_patio_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
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 (
@@ -1218,9 +1325,9 @@ class BlasRules:
)
)
def can_beat_wall_boss(self, state: CollectionState) -> bool:
def can_beat_wall_boss(self, state: CollectionState, player_strength: float | None = None) -> bool:
return (
self.has_boss_strength(state, "amanecida")
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 (
@@ -1244,8 +1351,7 @@ class BlasRules:
def can_beat_legionary(self, state: CollectionState) -> bool:
return self.has_boss_strength(state, "legionary")
def has_boss_strength(self, state: CollectionState, boss: str) -> bool:
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)
@@ -1259,30 +1365,16 @@ class BlasRules:
+ min(8, flasks) * 0.15 / 8
+ min(5, quicksilver) * 0.15 / 5
)
return player_strength
bosses: Dict[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
}
boss_strength: float = bosses[boss]
return player_strength >= (boss_strength - 0.10 if self.world.options.difficulty >= 2 else
(boss_strength if self.world.options.difficulty >= 1 else boss_strength + 0.10))
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) -> int:
doors = [
def guilt_rooms(self, state: CollectionState, count: int) -> bool:
doors = (
"D01Z04S01[NE]",
"D02Z02S11[W]",
"D03Z03S02[NE]",
@@ -1290,20 +1382,25 @@ class BlasRules:
"D05Z01S05[NE]",
"D09Z01S05[W]",
"D17Z01S04[W]",
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
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) -> int:
doors = [
["D01Z02S07[E]", "D01Z02S02[SW]"],
["D20Z01S04[E]", "D01Z05S23[W]"],
["D02Z03S02[NE]"],
["D04Z02S21[NE]"],
["D05Z01S21[NW]"],
["D06Z01S15[NE]"],
["D17Z01S07[SW]"]
]
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:
@@ -1311,72 +1408,90 @@ class BlasRules:
if state.can_reach_region(door, self.player):
total += 1
break
if total >= count:
return True
return total
return False
def redento_rooms(self, state: CollectionState) -> int:
if (
state.can_reach_region("D03Z01S04[E]", self.player)
or state.can_reach_region("D03Z02S10[N]", self.player)
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)
):
if (
# 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)
):
if (
state.can_reach_region("D01Z03S04[E]", self.player)
or state.can_reach_region("D08Z01S01[W]", self.player)
):
if (
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)
):
if (
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 5
return 4
return 3
return 2
return 1
return 0
def miriam_rooms(self, state: CollectionState) -> int:
doors = [
):
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]"
]
)
return sum(state.can_reach_region(door, self.player) for door in doors)
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):
if self.can_beat_graveyard_boss(state, player_strength):
total += 1
if self.can_beat_jondo_boss(state):
if self.can_beat_jondo_boss(state, player_strength):
total += 1
if self.can_beat_patio_boss(state):
if self.can_beat_patio_boss(state, player_strength):
total += 1
if self.can_beat_wall_boss(state):
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]"]
]
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:
+23 -2
View File
@@ -1,9 +1,9 @@
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, CollectionState
from Options import OptionError
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
from .Items import base_id, item_table, group_table, tears_list, reliquary_set, group_table_reverse
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
@@ -216,6 +216,27 @@ class BlasphemousWorld(World):
for loc, item in option_dict.items():
self.get_location(loc).place_locked_item(self.create_item(item))
def collect(self, state: CollectionState, item: Item) -> bool:
changed = super().collect(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 1:
# Count was 0 before super().collect().
group_name = group_table_reverse[name]
# Increase unique count for items in this group.
state.prog_items[self.player][group_name] += 1
return changed
def remove(self, state: CollectionState, item: Item) -> bool:
changed = super().remove(state, item)
if changed:
name = item.name
if name in group_table_reverse and state.count(name, self.player) == 0:
# Count was 1 before super().remove().
group_name = group_table_reverse[name]
# Decrease unique count for items in this group.
state.prog_items[self.player][group_name] -= 1
return changed
def create_regions(self) -> None:
multiworld = self.multiworld