Blasphemous: Total overhaul (#3355)

* Blasphemous: WIP overhaul

* Entrance rule mistake

* stuff

* Getting closer

* Real?? Maybe??

* Don't fail me now 🙏

* Add starting location tests

* More tests (it still doesn't work actually 😔)

* REAL

* Add unreachable regions to test_reachability.py

* PR ready

- Remove unused functions from init
- Use group exclusive functions in rules
- Style changes

* Bump required client version

* Clean up unused imports

* Change slot data

* Review fixes

- Prevent strength calculations from including excess items
- Add new lines to ends of files
- Fix missed deprecated option and random usage in init

* Update option docstrings, add groups

* Add preprocessor files

* Update option docstrings again actually

* Update player strength calculation

* Rename group methods

* Fix missing logic for RESCUED_CHERUB_06

* Register indirect conditions

* Register indirect conditions (part 2)

* Update extracted logic, change slot data key

* Add region to excluded list

* A capital letter

* Use camelCase keys in preprocessor

* Write some of new setup guide

* Remove indents before list points

* Change locationinfo to list of dictonaries

* Finish docs, update extractor config and data

* Mark region_data.py as generated

* Suggested changes

* More suggested changes

* Suggested changes again

- Use OptionError
- Create list of disabled locations before looping
- Check if options are equal to str instead of int
- Clean up start location override
- Reword some of setup guide
- Organize location list
- Remove unnecessary escaped quotes from option docstrings
- Add world type to test base

* C# moment

* Requested changes

* Update .gitattributes

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Trevor L 2024-08-20 17:18:28 -06:00 committed by GitHub
parent 0e6e359747
commit 54a7bb5664
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 50864 additions and 11109 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
worlds/blasphemous/region_data.py linguist-generated=true

View File

@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
},
# These Blasphemous regions are not reachable with default options
"Blasphemous": {
"D01Z04S13[SE]", # difficulty must be hard
"D01Z05S25[E]", # difficulty must be hard
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
"D04Z01S06[E]", # purified_hand must be true
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
"D05Z01S11[SW]", # difficulty must be hard
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
"D20Z02S11[NW]", # difficulty must be hard
"D20Z02S11[E]", # difficulty must be hard
},
"Ocarina of Time": {
"Prelude of Light Warp", # Prelude is not progression by default
"Serenade of Water Warp", # Serenade is not progression by default

View File

@ -0,0 +1,19 @@
{
"type": "WorldDefinition",
"configuration": "./output/StringWorldDefinition.json",
"emptyRegionsToKeep": [
"D17Z01S01",
"D01Z02S01",
"D02Z03S09",
"D03Z03S11",
"D04Z03S01",
"D06Z01S09",
"D20Z02S09",
"D09Z01S09[Cell24]",
"D09Z01S08[Cell7]",
"D09Z01S08[Cell18]",
"D09BZ01S01[Cell24]",
"D09BZ01S01[Cell17]",
"D09BZ01S01[Cell19]"
]
}

View File

@ -637,52 +637,35 @@ item_table: List[ItemDict] = [
'classification': ItemClassification.filler}
]
event_table: Dict[str, str] = {
"OpenedDCGateW": "D01Z05S24",
"OpenedDCGateE": "D01Z05S12",
"OpenedDCLadder": "D01Z05S20",
"OpenedWOTWCave": "D02Z01S06",
"RodeGOTPElevator": "D02Z02S11",
"OpenedConventLadder": "D02Z03S11",
"BrokeJondoBellW": "D03Z02S09",
"BrokeJondoBellE": "D03Z02S05",
"OpenedMOMLadder": "D04Z02S06",
"OpenedTSCGate": "D05Z02S11",
"OpenedARLadder": "D06Z01S23",
"BrokeBOTTCStatue": "D08Z01S02",
"OpenedWOTHPGate": "D09Z01S05",
"OpenedBOTSSLadder": "D17Z01S04"
}
group_table: Dict[str, Set[str]] = {
"wounds" : ["Holy Wound of Attrition",
"wounds" : {"Holy Wound of Attrition",
"Holy Wound of Contrition",
"Holy Wound of Compunction"],
"Holy Wound of Compunction"},
"masks" : ["Deformed Mask of Orestes",
"masks" : {"Deformed Mask of Orestes",
"Mirrored Mask of Dolphos",
"Embossed Mask of Crescente"],
"Embossed Mask of Crescente"},
"marks" : ["Mark of the First Refuge",
"marks" : {"Mark of the First Refuge",
"Mark of the Second Refuge",
"Mark of the Third Refuge"],
"Mark of the Third Refuge"},
"tirso" : ["Bouquet of Rosemary",
"tirso" : {"Bouquet of Rosemary",
"Incense Garlic",
"Olive Seeds",
"Dried Clove",
"Sooty Garlic",
"Bouquet of Thyme"],
"Bouquet of Thyme"},
"tentudia": ["Tentudia's Carnal Remains",
"tentudia": {"Tentudia's Carnal Remains",
"Remains of Tentudia's Hair",
"Tentudia's Skeletal Remains"],
"Tentudia's Skeletal Remains"},
"egg" : ["Melted Golden Coins",
"egg" : {"Melted Golden Coins",
"Torn Bridal Ribbon",
"Black Grieving Veil"],
"Black Grieving Veil"},
"bones" : ["Parietal bone of Lasser, the Inquisitor",
"bones" : {"Parietal bone of Lasser, the Inquisitor",
"Jaw of Ashgan, the Inquisitor",
"Cervical vertebra of Zicher, the Brewmaster",
"Clavicle of Dalhuisen, the Schoolchild",
@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
"Scaphoid of Fierce, the Leper",
"Anklebone of Weston, the Pilgrim",
"Calcaneum of Persian, the Bandit",
"Navicular of Kahnnyhoo, the Murderer"],
"Navicular of Kahnnyhoo, the Murderer"},
"power" : ["Life Upgrade",
"power" : {"Life Upgrade",
"Fervour Upgrade",
"Empty Bile Vessel",
"Quicksilver"],
"Quicksilver"},
"prayer" : ["Seguiriya to your Eyes like Stars",
"prayer" : {"Seguiriya to your Eyes like Stars",
"Debla of the Lights",
"Saeta Dolorosa",
"Campanillero to the Sons of the Aurora",
@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
"Romance to the Crimson Mist",
"Zambra to the Resplendent Crown",
"Cantina of the Blue Rose",
"Mirabras of the Return to Port"]
"Mirabras of the Return to Port"},
"toe" : {"Little Toe made of Limestone",
"Big Toe made of Limestone",
"Fourth Toe made of Limestone"},
"eye" : {"Severed Right Eye of the Traitor",
"Broken Left Eye of the Traitor"}
}
tears_set: Set[str] = [
tears_list: List[str] = [
"Tears of Atonement (500)",
"Tears of Atonement (625)",
"Tears of Atonement (750)",
@ -772,16 +762,16 @@ tears_set: Set[str] = [
"Tears of Atonement (30000)"
]
reliquary_set: Set[str] = [
reliquary_set: Set[str] = {
"Reliquary of the Fervent Heart",
"Reliquary of the Suffering Heart",
"Reliquary of the Sorrowful Heart"
]
}
skill_set: Set[str] = [
skill_set: Set[str] = {
"Combo Skill",
"Charged Skill",
"Ranged Skill",
"Dive Skill",
"Lunge Skill"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
from dataclasses import dataclass
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
import random
@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
class PrieDieuWarp(DefaultOnToggle):
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
"""
Automatically unlocks the ability to warp between Prie Dieu shrines.
"""
display_name = "Unlock Fast Travel"
class SkipCutscenes(DefaultOnToggle):
"""Automatically skips most cutscenes."""
"""
Automatically skips most cutscenes.
"""
display_name = "Auto Skip Cutscenes"
class CorpseHints(DefaultOnToggle):
"""Changes the 34 corpses in game to give various hints about item locations."""
"""
Changes the 34 corpses in game to give various hints about item locations.
"""
display_name = "Corpse Hints"
class Difficulty(Choice):
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
and advanced movement tricks or glitches."""
"""
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
"""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
@ -45,15 +53,18 @@ class Difficulty(Choice):
class Penitence(Toggle):
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
"""
Allows one of the three Penitences to be chosen at the beginning of the game.
"""
display_name = "Penitence"
class StartingLocation(ChoiceIsRandom):
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
cannot be chosen if Shuffle Wall Climb is enabled."""
"""
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends cannot be chosen if Shuffle Wall Climb is enabled.
"""
display_name = "Starting Location"
option_brotherhood = 0
option_albero = 1
@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
class Ending(Choice):
"""Choose which ending is required to complete the game.
"""
Choose which ending is required to complete the game.
Talking to Tirso in Albero will tell you the selected ending for the current game.
Ending A: Collect all thorn upgrades.
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
"""
display_name = "Ending"
option_any_ending = 0
option_ending_a = 1
@ -78,14 +94,18 @@ class Ending(Choice):
class SkipLongQuests(Toggle):
"""Ensures that the rewards for long quests will be filler items.
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
\"TSC: Jocinero's final reward\""""
"""
Ensures that the rewards for long quests will be filler items.
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
"""
display_name = "Skip Long Quests"
class ThornShuffle(Choice):
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
"""
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
"""
display_name = "Shuffle Thorn"
option_anywhere = 0
option_local_only = 1
@ -94,50 +114,68 @@ class ThornShuffle(Choice):
class DashShuffle(Toggle):
"""Turns the ability to dash into an item that must be found in the multiworld."""
"""
Turns the ability to dash into an item that must be found in the multiworld.
"""
display_name = "Shuffle Dash"
class WallClimbShuffle(Toggle):
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
"""
Turns the ability to climb walls with your sword into an item that must be found in the multiworld.
"""
display_name = "Shuffle Wall Climb"
class ReliquaryShuffle(DefaultOnToggle):
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
"""
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
"""
display_name = "Shuffle Penitence Rewards"
class CustomItem1(Toggle):
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
and survive.
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
"""
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
"""
display_name = "Boots of Pleading"
class CustomItem2(Toggle):
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
a second time in mid-air.
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
"""
Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump a second time in mid-air.
Must have the "Double Jump" mod installed to connect to a multiworld.
"""
display_name = "Purified Hand of the Nun"
class StartWheel(Toggle):
"""Changes the beginning gift to The Young Mason's Wheel."""
"""
Changes the beginning gift to The Young Mason's Wheel.
"""
display_name = "Start with Wheel"
class SkillRando(Toggle):
"""Randomizes the abilities from the skill tree into the item pool."""
"""
Randomizes the abilities from the skill tree into the item pool.
"""
display_name = "Skill Randomizer"
class EnemyRando(Choice):
"""Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
a standard game.
"""
Randomizes the enemies that appear in each room.
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
Randomized: Every enemy is completely random, and can appear any number of times.
Some enemies will never be randomized."""
Some enemies will never be randomized.
"""
display_name = "Enemy Randomizer"
option_disabled = 0
option_shuffled = 1
@ -146,43 +184,75 @@ class EnemyRando(Choice):
class EnemyGroups(DefaultOnToggle):
"""Randomized enemies will chosen from sets of specific groups.
"""
Randomized enemies will be chosen from sets of specific groups.
(Weak, normal, large, flying)
Has no effect if Enemy Randomizer is disabled."""
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Groups"
class EnemyScaling(DefaultOnToggle):
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled."""
"""
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
Has no effect if Enemy Randomizer is disabled.
"""
display_name = "Enemy Scaling"
class BlasphemousDeathLink(DeathLink):
"""When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link."""
"""
When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link.
"""
blasphemous_options = {
"prie_dieu_warp": PrieDieuWarp,
"skip_cutscenes": SkipCutscenes,
"corpse_hints": CorpseHints,
"difficulty": Difficulty,
"penitence": Penitence,
"starting_location": StartingLocation,
"ending": Ending,
"skip_long_quests": SkipLongQuests,
"thorn_shuffle" : ThornShuffle,
"dash_shuffle": DashShuffle,
"wall_climb_shuffle": WallClimbShuffle,
"reliquary_shuffle": ReliquaryShuffle,
"boots_of_pleading": CustomItem1,
"purified_hand": CustomItem2,
"start_wheel": StartWheel,
"skill_randomizer": SkillRando,
"enemy_randomizer": EnemyRando,
"enemy_groups": EnemyGroups,
"enemy_scaling": EnemyScaling,
"death_link": BlasphemousDeathLink,
"start_inventory": StartInventoryPool
}
@dataclass
class BlasphemousOptions(PerGameCommonOptions):
prie_dieu_warp: PrieDieuWarp
skip_cutscenes: SkipCutscenes
corpse_hints: CorpseHints
difficulty: Difficulty
penitence: Penitence
starting_location: StartingLocation
ending: Ending
skip_long_quests: SkipLongQuests
thorn_shuffle: ThornShuffle
dash_shuffle: DashShuffle
wall_climb_shuffle: WallClimbShuffle
reliquary_shuffle: ReliquaryShuffle
boots_of_pleading: CustomItem1
purified_hand: CustomItem2
start_wheel: StartWheel
skill_randomizer: SkillRando
enemy_randomizer: EnemyRando
enemy_groups: EnemyGroups
enemy_scaling: EnemyScaling
death_link: BlasphemousDeathLink
blas_option_groups = [
OptionGroup("Quality of Life", [
PrieDieuWarp,
SkipCutscenes,
CorpseHints,
SkipLongQuests,
StartWheel
]),
OptionGroup("Moveset", [
DashShuffle,
WallClimbShuffle,
SkillRando,
CustomItem1,
CustomItem2
]),
OptionGroup("Enemy Randomizer", [
EnemyRando,
EnemyGroups,
EnemyScaling
])
]

View File

@ -0,0 +1,582 @@
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
# https://github.com/BrandenEK/Blasphemous.Randomizer
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
import json, requests, argparse
from typing import List, Dict, Any
def load_resource_local(file: str) -> List[Dict[str, Any]]:
print(f"Reading from {file}")
loaded = []
with open(file, encoding="utf-8") as f:
loaded = read_json(f.readlines())
f.close()
return loaded
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
req = requests.get(url, timeout=1)
print(f"Reading from {url}")
req.encoding = "utf-8"
lines: List[str] = []
for line in req.text.splitlines():
while "\t" in line:
line = line[1::]
if line != "":
lines.append(line)
return read_json(lines)
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
loaded = []
creating_object: bool = False
obj: str = ""
for line in lines:
stripped = line.strip()
if "{" in stripped:
creating_object = True
obj += stripped
continue
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
creating_object = False
obj += "}"
#print(f"obj = {obj}")
loaded.append(json.loads(obj))
obj = ""
continue
if not creating_object:
continue
else:
try:
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
obj += stripped[:-1]
else:
obj += stripped
except IndexError:
obj += stripped
return loaded
def get_room_from_door(door: str) -> str:
return door[:door.find("[")]
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
if id in logic and not is_door:
index: int = logic.find(id)
logic = logic[:index] + logic[index+len(id)+4:]
while ">=" in logic:
index: int = logic.find(">=")
logic = logic[:index-1] + logic[index+3:]
while ">" in logic:
index: int = logic.find(">")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
while "<=" in logic:
index: int = logic.find("<=")
logic = logic[:index-1] + logic[index+3:]
while "<" in logic:
index: int = logic.find("<")
count = int(logic[index+2])
count += 1
logic = logic[:index-1] + str(count) + logic[index+3:]
#print(logic)
return logic
def build_logic_conditions(logic: str) -> List[List[str]]:
all_conditions: List[List[str]] = []
parts = logic.split()
sub_part: str = ""
current_index: int = 0
parens: int = -1
current_condition: List[str] = []
parens_conditions: List[List[List[str]]] = []
for index, part in enumerate(parts):
#print(current_index, index, parens, part)
# skip parts that have already been handled
if index < current_index:
continue
# break loop if reached final part
try:
parts[index+1]
except IndexError:
#print("INDEXERROR", part)
if parens < 0:
current_condition.append(part)
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
else:
all_conditions.append(current_condition)
break
#print(current_condition, parens, sub_part)
# prepare for subcondition
if "(" in part:
# keep track of nested parentheses
if parens == -1:
parens = 0
for char in part:
if char == "(":
parens += 1
# add to sub part
if sub_part == "":
sub_part = part
else:
sub_part += f" {part}"
#if not ")" in part:
continue
# end of subcondition
if ")" in part:
# read every character in case of multiple closing parentheses
for char in part:
if char == ")":
parens -= 1
sub_part += f" {part}"
# if reached end of parentheses, handle subcondition
if parens == 0:
#print(current_condition, sub_part)
parens = -1
try:
parts[index+1]
except IndexError:
#print("END OF LOGIC")
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
else:
#print("NEXT PARTS:", parts[index+1], parts[index+2])
if parts[index+1] == "&&":
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
else:
if len(parens_conditions) > 0:
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
#print("PARENS:", parens_conditions)
temp_conditions: List[List[str]] = []
for i in parens_conditions[0]:
for j in parens_conditions[1]:
temp_conditions.append(i + j)
parens_conditions.pop(0)
parens_conditions.pop(0)
while len(parens_conditions) > 0:
temp_conditions2 = temp_conditions
temp_conditions = []
for k in temp_conditions2:
for l in parens_conditions[0]:
temp_conditions.append(k + l)
parens_conditions.pop(0)
#print("TEMP:", remove_duplicates(temp_conditions))
all_conditions += temp_conditions
else:
all_conditions += build_logic_subconditions(current_condition, sub_part)
current_index = index+2
current_condition = []
sub_part = ""
continue
# collect all parts until reaching end of parentheses
if parens > 0:
sub_part += f" {part}"
continue
current_condition.append(part)
# continue with current condition
if parts[index+1] == "&&":
current_index = index+2
continue
# add condition to list and start new one
elif parts[index+1] == "||":
if len(parens_conditions) > 0:
for i in parens_conditions:
for j in i:
all_conditions.append(j + current_condition)
parens_conditions = []
else:
all_conditions.append(current_condition)
current_condition = []
current_index = index+2
continue
return remove_duplicates(all_conditions)
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
#print("STARTED SUBCONDITION", current_condition, subcondition)
subconditions = build_logic_conditions(subcondition[1:-1])
final_conditions = []
for condition in subconditions:
final_condition = current_condition + condition
final_conditions.append(final_condition)
#print("ENDED SUBCONDITION")
#print(final_conditions)
return final_conditions
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
final_conditions: List[List[str]] = []
for condition in conditions:
final_conditions.append(list(dict.fromkeys(condition)))
return final_conditions
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
if door.get("visibilityFlags") == None:
return door
else:
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
#print(flags)
temp_flags: List[str] = []
this_door: bool = False
#required_doors: str = ""
if "ThisDoor" in flags:
this_door = True
#if "requiredDoors" in flags:
# required_doors: str = " || ".join(door.get("requiredDoors"))
if "DoubleJump" in flags:
temp_flags.append("DoubleJump")
if "NormalLogic" in flags:
temp_flags.append("NormalLogic")
if "NormalLogicAndDoubleJump" in flags:
temp_flags.append("NormalLogicAndDoubleJump")
if "HardLogic" in flags:
temp_flags.append("HardLogic")
if "HardLogicAndDoubleJump" in flags:
temp_flags.append("HardLogicAndDoubleJump")
if "EnemySkips" in flags:
temp_flags.append("EnemySkips")
if "EnemySkipsAndDoubleJump" in flags:
temp_flags.append("EnemySkipsAndDoubleJump")
# remove duplicates
temp_flags = list(dict.fromkeys(temp_flags))
original_logic: str = door.get("logic")
temp_logic: str = ""
if this_door:
temp_logic = door.get("id")
if temp_flags != []:
if temp_logic != "":
temp_logic += " || "
temp_logic += ' && '.join(temp_flags)
if temp_logic != "" and original_logic != None:
if len(original_logic.split()) == 1:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && {original_logic}"
else:
door["logic"] = f"({temp_logic}) && {original_logic}"
else:
if len(temp_logic.split()) == 1:
door["logic"] = f"{temp_logic} && ({original_logic})"
else:
door["logic"] = f"({temp_logic}) && ({original_logic})"
elif temp_logic != "" and original_logic == None:
door["logic"] = temp_logic
return door
def get_state_provider_for_condition(condition: List[str]) -> str:
for item in condition:
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
return item
return None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
args = parser.parse_args()
return args
def main(args: argparse.Namespace):
doors = []
locations = []
if (args.local):
doors = load_resource_local("doors.json")
locations = load_resource_local("locations_items.json")
else:
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
original_connections: Dict[str, str] = {}
rooms: Dict[str, List[str]] = {}
output: Dict[str, Any] = {}
logic_objects: List[Dict[str, Any]] = []
for door in doors:
if door.get("originalDoor") != None:
if not door.get("id") in original_connections:
original_connections[door.get("id")] = door.get("originalDoor")
original_connections[door.get("originalDoor")] = door.get("id")
room: str = get_room_from_door(door.get("originalDoor"))
if not room in rooms.keys():
rooms[room] = [door.get("id")]
else:
rooms[room].append(door.get("id"))
def flip_doors_in_condition(condition: List[str]) -> List[str]:
new_condition = []
for item in condition:
if item in original_connections:
new_condition.append(original_connections[item])
else:
new_condition.append(item)
return new_condition
for room in rooms.keys():
obj = {
"Name": room,
"Logic": [],
"Handling": "Default"
}
for door in rooms[room]:
logic = {
"StateProvider": door,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for door in doors:
if door.get("direction") == 5:
continue
handling: str = "Transition"
if "Cell" in door.get("id"):
handling = "Default"
obj = {
"Name": door.get("id"),
"Logic": [],
"Handling": handling
}
visibility_flags: List[str] = []
if door.get("visibilityFlags") != None:
visibility_flags = str(door.get("visibilityFlags")).split(", ")
if "1" in visibility_flags:
visibility_flags.remove("1")
visibility_flags.append("ThisDoor")
required_doors: List[str] = []
if door.get("requiredDoors"):
required_doors = door.get("requiredDoors")
if len(visibility_flags) > 0:
for flag in visibility_flags:
if flag == "RequiredDoors":
continue
if flag == "ThisDoor":
flag = original_connections[door.get("id")]
if door.get("logic") != None:
logic: str = door.get("logic")
logic = f"{flag} && ({logic})"
logic = preprocess_logic(True, door.get("id"), logic)
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
state_provider = get_state_provider_for_condition(condition)
condition.remove(state_provider)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [flag],
"StateModifiers": []
}
obj["Logic"].append(logic)
if "RequiredDoors" in visibility_flags:
for d in required_doors:
flipped = original_connections[d]
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
state_provider: str = flipped
if flipped in condition:
condition.remove(flipped)
logic = {
"StateProvider": state_provider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": flipped,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
if door.get("logic") != None:
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
conditions = build_logic_conditions(logic)
for condition in conditions:
condition = flip_doors_in_condition(condition)
stateProvider: str = get_room_from_door(door.get("id"))
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
logic = {
"StateProvider": get_room_from_door(door.get("id")),
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
for location in locations:
obj = {
"Name": location.get("id"),
"Logic": [],
"Handling": "Location"
}
if location.get("logic") != None:
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
condition = flip_doors_in_condition(condition)
stateProvider: str = location.get("room")
if get_state_provider_for_condition(condition) != None:
stateProvider = get_state_provider_for_condition(condition)
condition.remove(stateProvider)
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": condition,
"StateModifiers": []
}
obj["Logic"].append(logic)
else:
stateProvider: str = location.get("room")
if stateProvider == "Initial":
stateProvider = None
logic = {
"StateProvider": stateProvider,
"Conditions": [],
"StateModifiers": []
}
obj["Logic"].append(logic)
logic_objects.append(obj)
output["LogicObjects"] = logic_objects
with open("StringWorldDefinition.json", "w") as file:
print("Writing to StringWorldDefinition.json")
file.write(json.dumps(output, indent=4))
if __name__ == "__main__":
main(parse_args())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
}
junk_locations: Set[str] = [
junk_locations: Set[str] = {
"Albero: Donate 50000 Tears",
"Ossuary: 11th reward",
"AtTotS: Miriam's gift",
"TSC: Jocinero's final reward"
]
}
thorn_set: Set[str] = {
@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
"Skill 5, Tier 1": "Lunge Skill",
"Skill 5, Tier 2": "Lunge Skill",
"Skill 5, Tier 3": "Lunge Skill",
}
}

View File

@ -1,15 +1,15 @@
from typing import Dict, List, Set, Any
from collections import Counter
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from Options import OptionError
from worlds.AutoWorld import World, WebWorld
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
from .Locations import location_table
from .Rooms import room_table, door_table
from .Rules import rules
from worlds.generic.Rules import set_rule, add_rule
from .Options import blasphemous_options
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
from .Locations import location_names
from .Rules import BlasRules
from worlds.generic.Rules import set_rule
from .Options import BlasphemousOptions, blas_option_groups
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
from .region_data import regions, locations
class BlasphemousWeb(WebWorld):
theme = "stone"
@ -21,39 +21,33 @@ class BlasphemousWeb(WebWorld):
"setup/en",
["TRPG"]
)]
option_groups = blas_option_groups
class BlasphemousWorld(World):
"""
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
your eternal damnation!
"""
game: str = "Blasphemous"
game = "Blasphemous"
web = BlasphemousWeb()
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
item_name_groups = group_table
option_definitions = blasphemous_options
options_dataclass = BlasphemousOptions
options: BlasphemousOptions
required_client_version = (0, 4, 2)
required_client_version = (0, 4, 7)
def __init__(self, multiworld, player):
super(BlasphemousWorld, self).__init__(multiworld, player)
self.start_room: str = "D17Z01S01"
self.door_connections: Dict[str, str] = {}
def set_rules(self):
rules(self)
for door in door_table:
add_rule(self.multiworld.get_location(door["Id"], self.player),
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
self.disabled_locations: List[str] = []
def create_item(self, name: str) -> "BlasphemousItem":
@ -68,64 +62,56 @@ class BlasphemousWorld(World):
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(tears_set)
return self.random.choice(tears_list)
def generate_early(self):
world = self.multiworld
player = self.player
if not self.options.starting_location.randomized:
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
if not world.starting_location[player].randomized:
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Difficulty is lower than Hard.")
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Shuffle Dash is enabled.")
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
and self.options.dash_shuffle:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
" cannot be chosen if Shuffle Wall Climb is enabled.")
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
else:
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
invalid: bool = False
if world.difficulty[player].value < 2:
if self.options.difficulty < 2:
locations.remove(6)
if world.dash_shuffle[player]:
if self.options.dash_shuffle:
locations.remove(0)
if 6 in locations:
locations.remove(6)
if world.wall_climb_shuffle[player]:
if self.options.wall_climb_shuffle:
locations.remove(3)
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
invalid = True
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
and world.dash_shuffle[player]:
invalid = True
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
invalid = True
if invalid:
world.starting_location[player].value = world.random.choice(locations)
if self.options.starting_location.value not in locations:
self.options.starting_location.value = self.random.choice(locations)
if not world.dash_shuffle[player]:
world.push_precollected(self.create_item("Dash Ability"))
if not self.options.dash_shuffle:
self.multiworld.push_precollected(self.create_item("Dash Ability"))
if not world.wall_climb_shuffle[player]:
world.push_precollected(self.create_item("Wall Climb Ability"))
if not self.options.wall_climb_shuffle:
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
if world.skip_long_quests[player]:
if not self.options.boots_of_pleading:
self.disabled_locations.append("RE401")
if not self.options.purified_hand:
self.disabled_locations.append("RE402")
if self.options.skip_long_quests:
for loc in junk_locations:
world.exclude_locations[player].value.add(loc)
self.options.exclude_locations.value.add(loc)
start_rooms: Dict[int, str] = {
0: "D17Z01S01",
@ -137,13 +123,10 @@ class BlasphemousWorld(World):
6: "D20Z02S09"
}
self.start_room = start_rooms[world.starting_location[player].value]
self.start_room = start_rooms[self.options.starting_location.value]
def create_items(self):
world = self.multiworld
player = self.player
removed: int = 0
to_remove: List[str] = [
"Tears of Atonement (250)",
@ -156,46 +139,46 @@ class BlasphemousWorld(World):
skipped_items = []
junk: int = 0
for item, count in world.start_inventory[player].value.items():
for item, count in self.options.start_inventory.value.items():
for _ in range(count):
skipped_items.append(item)
junk += 1
skipped_items.extend(unrandomized_dict.values())
if world.thorn_shuffle[player] == 2:
for i in range(8):
if self.options.thorn_shuffle == "vanilla":
for _ in range(8):
skipped_items.append("Thorn Upgrade")
if world.dash_shuffle[player]:
if self.options.dash_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.dash_shuffle[player]:
elif not self.options.dash_shuffle:
skipped_items.append("Dash Ability")
if world.wall_climb_shuffle[player]:
if self.options.wall_climb_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not world.wall_climb_shuffle[player]:
elif not self.options.wall_climb_shuffle:
skipped_items.append("Wall Climb Ability")
if not world.reliquary_shuffle[player]:
if not self.options.reliquary_shuffle:
skipped_items.extend(reliquary_set)
elif world.reliquary_shuffle[player]:
for i in range(3):
elif self.options.reliquary_shuffle:
for _ in range(3):
skipped_items.append(to_remove[removed])
removed += 1
if not world.boots_of_pleading[player]:
if not self.options.boots_of_pleading:
skipped_items.append("Boots of Pleading")
if not world.purified_hand[player]:
if not self.options.purified_hand:
skipped_items.append("Purified Hand of the Nun")
if world.start_wheel[player]:
if self.options.start_wheel:
skipped_items.append("The Young Mason's Wheel")
if not world.skill_randomizer[player]:
if not self.options.skill_randomizer:
skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items)
@ -208,184 +191,140 @@ class BlasphemousWorld(World):
if count <= 0:
continue
else:
for i in range(count):
for _ in range(count):
pool.append(self.create_item(item["name"]))
for _ in range(junk):
pool.append(self.create_item(self.get_filler_item_name()))
world.itempool += pool
self.multiworld.itempool += pool
def pre_fill(self):
world = self.multiworld
player = self.player
self.place_items_from_dict(unrandomized_dict)
if world.thorn_shuffle[player] == 2:
if self.options.thorn_shuffle == "vanilla":
self.place_items_from_set(thorn_set, "Thorn Upgrade")
if world.start_wheel[player]:
world.get_location("Beginning gift", player)\
.place_locked_item(self.create_item("The Young Mason's Wheel"))
if self.options.start_wheel:
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
if not world.skill_randomizer[player]:
if not self.options.skill_randomizer:
self.place_items_from_dict(skill_dict)
if world.thorn_shuffle[player] == 1:
world.local_items[player].value.add("Thorn Upgrade")
if self.options.thorn_shuffle == "local_only":
self.options.local_items.value.add("Thorn Upgrade")
def place_items_from_set(self, location_set: Set[str], name: str):
for loc in location_set:
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(name))
self.get_location(loc).place_locked_item(self.create_item(name))
def place_items_from_dict(self, option_dict: Dict[str, str]):
for loc, item in option_dict.items():
self.multiworld.get_location(loc, self.player)\
.place_locked_item(self.create_item(item))
self.get_location(loc).place_locked_item(self.create_item(item))
def create_regions(self) -> None:
multiworld = self.multiworld
player = self.player
world = self.multiworld
created_regions: List[str] = []
for r in regions:
multiworld.regions.append(Region(r["name"], player, multiworld))
created_regions.append(r["name"])
self.get_region("Menu").add_exits({self.start_room: "New Game"})
blas_logic = BlasRules(self)
for r in regions:
region = self.get_region(r["name"])
for e in r["exits"]:
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
for t in r["transitions"]:
if t == r["name"]:
continue
if t in created_regions:
region.add_exits({t})
else:
multiworld.regions.append(Region(t, player, multiworld))
created_regions.append(t)
region.add_exits({t})
for l in [l for l in locations if l["name"] not in self.disabled_locations]:
location = self.get_location(location_names[l["name"]])
set_rule(location, blas_logic.load_rule(False, l["name"], l))
for rname, ename in blas_logic.indirect_conditions:
self.multiworld.register_indirect_condition(self.get_region(rname), self.get_entrance(ename))
#from Utils import visualize_regions
#visualize_regions(self.get_region("Menu"), "blasphemous_regions.puml")
menu_region = Region("Menu", player, world)
misc_region = Region("Misc", player, world)
world.regions += [menu_region, misc_region]
for room in room_table:
region = Region(room, player, world)
world.regions.append(region)
menu_region.add_exits({self.start_room: "New Game"})
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
for door in door_table:
if door.get("OriginalDoor") is None:
continue
else:
if not door["Id"] in self.door_connections.keys():
self.door_connections[door["Id"]] = door["OriginalDoor"]
self.door_connections[door["OriginalDoor"]] = door["Id"]
parent_region: Region = self.get_room_from_door(door["Id"])
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
parent_region.add_exits({
target_region.name: door["Id"]
}, {
target_region.name: lambda x: door.get("VisibilityFlags") != 1
})
for index, loc in enumerate(location_table):
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
continue
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
continue
region: Region = world.get_region(loc["room"], player)
region.add_locations({loc["name"]: base_id + index})
#id = base_id + location_table.index(loc)
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
for e, r in event_table.items():
region: Region = world.get_region(r, player)
event = BlasphemousLocation(player, e, None, region)
event.show_in_spoiler = False
event.place_locked_item(self.create_event(e))
region.locations.append(event)
for door in door_table:
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
event = BlasphemousLocation(player, door["Id"], None, region)
event.show_in_spoiler = False
event.place_locked_item(self.create_event(door["Id"]))
region.locations.append(event)
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
victory.place_locked_item(self.create_event("Victory"))
world.get_region("D07Z01S03", player).locations.append(victory)
self.get_region("D07Z01S03[W]").locations.append(victory)
if world.ending[self.player].value == 1:
if self.options.ending == "ending_a":
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
elif world.ending[self.player].value == 2:
elif self.options.ending == "ending_c":
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
state.has("Holy Wound of Abnegation", player))
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
def get_room_from_door(self, door: str) -> Region:
return self.multiworld.get_region(door.split("[")[0], self.player)
def get_connected_door(self, door: str) -> Entrance:
return self.multiworld.get_entrance(self.door_connections[door], self.player)
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {}
locations = []
doors: Dict[str, str] = {}
world = self.multiworld
player = self.player
thorns: bool = True
if world.thorn_shuffle[player].value == 2:
if self.options.thorn_shuffle == "vanilla":
thorns = False
for loc in world.get_filled_locations(player):
if loc.item.code == None:
continue
else:
data = {
"id": self.location_name_to_game_id[loc.name],
"ap_id": loc.address,
"name": loc.item.name,
"player_name": world.player_name[loc.item.player],
"type": int(loc.item.classification)
}
locations.append(data)
config = {
"LogicDifficulty": world.difficulty[player].value,
"StartingLocation": world.starting_location[player].value,
"LogicDifficulty": self.options.difficulty.value,
"StartingLocation": self.options.starting_location.value,
"VersionCreated": "AP",
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
"AllowHints": bool(world.corpse_hints[player].value),
"AllowPenitence": bool(world.penitence[player].value),
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
"AllowHints": bool(self.options.corpse_hints.value),
"AllowPenitence": bool(self.options.penitence.value),
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
"ShuffleDash": bool(world.dash_shuffle[player].value),
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
"ShuffleDash": bool(self.options.dash_shuffle.value),
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
"ShuffleThorns": thorns,
"JunkLongQuests": bool(world.skip_long_quests[player].value),
"StartWithWheel": bool(world.start_wheel[player].value),
"JunkLongQuests": bool(self.options.skip_long_quests.value),
"StartWithWheel": bool(self.options.start_wheel.value),
"EnemyShuffleType": world.enemy_randomizer[player].value,
"MaintainClass": bool(world.enemy_groups[player].value),
"AreaScaling": bool(world.enemy_scaling[player].value),
"EnemyShuffleType": self.options.enemy_randomizer.value,
"MaintainClass": bool(self.options.enemy_groups.value),
"AreaScaling": bool(self.options.enemy_scaling.value),
"BossShuffleType": 0,
"DoorShuffleType": 0
}
slot_data = {
"locations": locations,
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
"doors": doors,
"cfg": config,
"ending": world.ending[self.player].value,
"death_link": bool(world.death_link[self.player].value)
"ending": self.options.ending.value,
"death_link": bool(self.options.death_link.value)
}
return slot_data

View File

@ -1,48 +1,17 @@
# Blasphemous Multiworld Setup Guide
## Useful Links
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
Required:
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
- The GOG version of Blasphemous will also work.
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
Optional:
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
Some optional mods are also recommended:
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
## Mod Installer (Recommended)
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
and point it to your install directory for Blasphemous.
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Manual Installation
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
2. After the Modding API has been installed, download the
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
into the `Modding` folder. Then, add any desired additional mods.
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
the Randomizer and Multiworld on the title screen.
## Connecting
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
the command `multiworld connect [address:port] [name] [password]`.
The port and password are both optional - if no port is provided then the default port of 38281 is used.
**Make sure to connect to the server before attempting to start a new save file.**
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
- `ap status` - Display connection status.
- `ap say [message]` - Send a message to the server.
- `ap hint [item]` - Request a hint for an item from the server.

48070
worlds/blasphemous/region_data.py generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
from test.bases import WorldTestBase
from .. import BlasphemousWorld
class BlasphemousTestBase(WorldTestBase):
game = "Blasphemous"
world: BlasphemousWorld

View File

@ -0,0 +1,56 @@
from . import BlasphemousTestBase
from ..Locations import location_names
class BotSSGauntletTest(BlasphemousTestBase):
options = {
"starting_location": "albero",
"wall_climb_shuffle": True,
"dash_shuffle": True
}
@property
def run_default_tests(self) -> bool:
return False
def test_botss_gauntlet(self) -> None:
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
class BackgroundZonesTest(BlasphemousTestBase):
@property
def run_default_tests(self) -> bool:
return False
def test_dc_shroud(self) -> None:
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
def test_wothp_bronze_cells(self) -> None:
bronze_locations = [
location_names["QI70"],
location_names["RESCUED_CHERUB_03"]
]
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
def test_wothp_silver_cells(self) -> None:
silver_locations = [
location_names["CO24"],
location_names["RESCUED_CHERUB_34"],
location_names["CO37"],
location_names["RESCUED_CHERUB_04"]
]
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
def test_wothp_gold_cells(self) -> None:
gold_locations = [
location_names["QI51"],
location_names["CO26"],
location_names["CO02"]
]
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
def test_wothp_quirce(self) -> None:
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)

View File

@ -0,0 +1,135 @@
from . import BlasphemousTestBase
class TestBrotherhoodEasy(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "easy"
}
class TestBrotherhoodNormal(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "normal"
}
class TestBrotherhoodHard(BlasphemousTestBase):
options = {
"starting_location": "brotherhood",
"difficulty": "hard"
}
class TestAlberoEasy(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "easy"
}
class TestAlberoNormal(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "normal"
}
class TestAlberoHard(BlasphemousTestBase):
options = {
"starting_location": "albero",
"difficulty": "hard"
}
class TestConventEasy(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "easy"
}
class TestConventNormal(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "normal"
}
class TestConventHard(BlasphemousTestBase):
options = {
"starting_location": "convent",
"difficulty": "hard"
}
class TestGrievanceEasy(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "easy"
}
class TestGrievanceNormal(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "normal"
}
class TestGrievanceHard(BlasphemousTestBase):
options = {
"starting_location": "grievance",
"difficulty": "hard"
}
class TestKnotOfWordsEasy(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "easy"
}
class TestKnotOfWordsNormal(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "normal"
}
class TestKnotOfWordsHard(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "hard"
}
class TestRooftopsEasy(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "easy"
}
class TestRooftopsNormal(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "normal"
}
class TestRooftopsHard(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "hard"
}
# mourning and havoc can't be selected on easy or normal. hard only
class TestMourningHavocHard(BlasphemousTestBase):
options = {
"starting_location": "mourning_havoc",
"difficulty": "hard"
}