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:
parent
0e6e359747
commit
54a7bb5664
|
@ -0,0 +1 @@
|
||||||
|
worlds/blasphemous/region_data.py linguist-generated=true
|
|
@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
|
||||||
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
||||||
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
|
"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": {
|
"Ocarina of Time": {
|
||||||
"Prelude of Light Warp", # Prelude is not progression by default
|
"Prelude of Light Warp", # Prelude is not progression by default
|
||||||
"Serenade of Water Warp", # Serenade is not progression by default
|
"Serenade of Water Warp", # Serenade is not progression by default
|
||||||
|
|
|
@ -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]"
|
||||||
|
]
|
||||||
|
}
|
|
@ -637,52 +637,35 @@ item_table: List[ItemDict] = [
|
||||||
'classification': ItemClassification.filler}
|
'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]] = {
|
group_table: Dict[str, Set[str]] = {
|
||||||
"wounds" : ["Holy Wound of Attrition",
|
"wounds" : {"Holy Wound of Attrition",
|
||||||
"Holy Wound of Contrition",
|
"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",
|
"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 Second Refuge",
|
||||||
"Mark of the Third Refuge"],
|
"Mark of the Third Refuge"},
|
||||||
|
|
||||||
"tirso" : ["Bouquet of Rosemary",
|
"tirso" : {"Bouquet of Rosemary",
|
||||||
"Incense Garlic",
|
"Incense Garlic",
|
||||||
"Olive Seeds",
|
"Olive Seeds",
|
||||||
"Dried Clove",
|
"Dried Clove",
|
||||||
"Sooty Garlic",
|
"Sooty Garlic",
|
||||||
"Bouquet of Thyme"],
|
"Bouquet of Thyme"},
|
||||||
|
|
||||||
"tentudia": ["Tentudia's Carnal Remains",
|
"tentudia": {"Tentudia's Carnal Remains",
|
||||||
"Remains of Tentudia's Hair",
|
"Remains of Tentudia's Hair",
|
||||||
"Tentudia's Skeletal Remains"],
|
"Tentudia's Skeletal Remains"},
|
||||||
|
|
||||||
"egg" : ["Melted Golden Coins",
|
"egg" : {"Melted Golden Coins",
|
||||||
"Torn Bridal Ribbon",
|
"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",
|
"Jaw of Ashgan, the Inquisitor",
|
||||||
"Cervical vertebra of Zicher, the Brewmaster",
|
"Cervical vertebra of Zicher, the Brewmaster",
|
||||||
"Clavicle of Dalhuisen, the Schoolchild",
|
"Clavicle of Dalhuisen, the Schoolchild",
|
||||||
|
@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
|
||||||
"Scaphoid of Fierce, the Leper",
|
"Scaphoid of Fierce, the Leper",
|
||||||
"Anklebone of Weston, the Pilgrim",
|
"Anklebone of Weston, the Pilgrim",
|
||||||
"Calcaneum of Persian, the Bandit",
|
"Calcaneum of Persian, the Bandit",
|
||||||
"Navicular of Kahnnyhoo, the Murderer"],
|
"Navicular of Kahnnyhoo, the Murderer"},
|
||||||
|
|
||||||
"power" : ["Life Upgrade",
|
"power" : {"Life Upgrade",
|
||||||
"Fervour Upgrade",
|
"Fervour Upgrade",
|
||||||
"Empty Bile Vessel",
|
"Empty Bile Vessel",
|
||||||
"Quicksilver"],
|
"Quicksilver"},
|
||||||
|
|
||||||
"prayer" : ["Seguiriya to your Eyes like Stars",
|
"prayer" : {"Seguiriya to your Eyes like Stars",
|
||||||
"Debla of the Lights",
|
"Debla of the Lights",
|
||||||
"Saeta Dolorosa",
|
"Saeta Dolorosa",
|
||||||
"Campanillero to the Sons of the Aurora",
|
"Campanillero to the Sons of the Aurora",
|
||||||
|
@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
|
||||||
"Romance to the Crimson Mist",
|
"Romance to the Crimson Mist",
|
||||||
"Zambra to the Resplendent Crown",
|
"Zambra to the Resplendent Crown",
|
||||||
"Cantina of the Blue Rose",
|
"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 (500)",
|
||||||
"Tears of Atonement (625)",
|
"Tears of Atonement (625)",
|
||||||
"Tears of Atonement (750)",
|
"Tears of Atonement (750)",
|
||||||
|
@ -772,16 +762,16 @@ tears_set: Set[str] = [
|
||||||
"Tears of Atonement (30000)"
|
"Tears of Atonement (30000)"
|
||||||
]
|
]
|
||||||
|
|
||||||
reliquary_set: Set[str] = [
|
reliquary_set: Set[str] = {
|
||||||
"Reliquary of the Fervent Heart",
|
"Reliquary of the Fervent Heart",
|
||||||
"Reliquary of the Suffering Heart",
|
"Reliquary of the Suffering Heart",
|
||||||
"Reliquary of the Sorrowful Heart"
|
"Reliquary of the Sorrowful Heart"
|
||||||
]
|
}
|
||||||
|
|
||||||
skill_set: Set[str] = [
|
skill_set: Set[str] = {
|
||||||
"Combo Skill",
|
"Combo Skill",
|
||||||
"Charged Skill",
|
"Charged Skill",
|
||||||
"Ranged Skill",
|
"Ranged Skill",
|
||||||
"Dive Skill",
|
"Dive Skill",
|
||||||
"Lunge Skill"
|
"Lunge Skill"
|
||||||
]
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
|
||||||
|
|
||||||
|
|
||||||
class PrieDieuWarp(DefaultOnToggle):
|
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"
|
display_name = "Unlock Fast Travel"
|
||||||
|
|
||||||
|
|
||||||
class SkipCutscenes(DefaultOnToggle):
|
class SkipCutscenes(DefaultOnToggle):
|
||||||
"""Automatically skips most cutscenes."""
|
"""
|
||||||
|
Automatically skips most cutscenes.
|
||||||
|
"""
|
||||||
display_name = "Auto Skip Cutscenes"
|
display_name = "Auto Skip Cutscenes"
|
||||||
|
|
||||||
|
|
||||||
class CorpseHints(DefaultOnToggle):
|
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"
|
display_name = "Corpse Hints"
|
||||||
|
|
||||||
|
|
||||||
class Difficulty(Choice):
|
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"
|
display_name = "Difficulty"
|
||||||
option_easy = 0
|
option_easy = 0
|
||||||
option_normal = 1
|
option_normal = 1
|
||||||
|
@ -45,15 +53,18 @@ class Difficulty(Choice):
|
||||||
|
|
||||||
|
|
||||||
class Penitence(Toggle):
|
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"
|
display_name = "Penitence"
|
||||||
|
|
||||||
|
|
||||||
class StartingLocation(ChoiceIsRandom):
|
class StartingLocation(ChoiceIsRandom):
|
||||||
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
|
"""
|
||||||
other options.
|
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."""
|
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"
|
display_name = "Starting Location"
|
||||||
option_brotherhood = 0
|
option_brotherhood = 0
|
||||||
option_albero = 1
|
option_albero = 1
|
||||||
|
@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
|
||||||
|
|
||||||
|
|
||||||
class Ending(Choice):
|
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.
|
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||||
|
|
||||||
Ending A: Collect all thorn upgrades.
|
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"
|
display_name = "Ending"
|
||||||
option_any_ending = 0
|
option_any_ending = 0
|
||||||
option_ending_a = 1
|
option_ending_a = 1
|
||||||
|
@ -78,14 +94,18 @@ class Ending(Choice):
|
||||||
|
|
||||||
|
|
||||||
class SkipLongQuests(Toggle):
|
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\",
|
Ensures that the rewards for long quests will be filler items.
|
||||||
\"TSC: Jocinero's final reward\""""
|
|
||||||
|
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
|
||||||
|
"""
|
||||||
display_name = "Skip Long Quests"
|
display_name = "Skip Long Quests"
|
||||||
|
|
||||||
|
|
||||||
class ThornShuffle(Choice):
|
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"
|
display_name = "Shuffle Thorn"
|
||||||
option_anywhere = 0
|
option_anywhere = 0
|
||||||
option_local_only = 1
|
option_local_only = 1
|
||||||
|
@ -94,50 +114,68 @@ class ThornShuffle(Choice):
|
||||||
|
|
||||||
|
|
||||||
class DashShuffle(Toggle):
|
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"
|
display_name = "Shuffle Dash"
|
||||||
|
|
||||||
|
|
||||||
class WallClimbShuffle(Toggle):
|
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"
|
display_name = "Shuffle Wall Climb"
|
||||||
|
|
||||||
|
|
||||||
class ReliquaryShuffle(DefaultOnToggle):
|
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"
|
display_name = "Shuffle Penitence Rewards"
|
||||||
|
|
||||||
|
|
||||||
class CustomItem1(Toggle):
|
class CustomItem1(Toggle):
|
||||||
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
|
"""
|
||||||
and survive.
|
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."""
|
|
||||||
|
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
|
||||||
|
"""
|
||||||
display_name = "Boots of Pleading"
|
display_name = "Boots of Pleading"
|
||||||
|
|
||||||
|
|
||||||
class CustomItem2(Toggle):
|
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.
|
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."""
|
|
||||||
|
Must have the "Double Jump" mod installed to connect to a multiworld.
|
||||||
|
"""
|
||||||
display_name = "Purified Hand of the Nun"
|
display_name = "Purified Hand of the Nun"
|
||||||
|
|
||||||
|
|
||||||
class StartWheel(Toggle):
|
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"
|
display_name = "Start with Wheel"
|
||||||
|
|
||||||
|
|
||||||
class SkillRando(Toggle):
|
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"
|
display_name = "Skill Randomizer"
|
||||||
|
|
||||||
|
|
||||||
class EnemyRando(Choice):
|
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
|
Randomizes the enemies that appear in each room.
|
||||||
a standard game.
|
|
||||||
|
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.
|
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"
|
display_name = "Enemy Randomizer"
|
||||||
option_disabled = 0
|
option_disabled = 0
|
||||||
option_shuffled = 1
|
option_shuffled = 1
|
||||||
|
@ -146,43 +184,75 @@ class EnemyRando(Choice):
|
||||||
|
|
||||||
|
|
||||||
class EnemyGroups(DefaultOnToggle):
|
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)
|
(Weak, normal, large, flying)
|
||||||
Has no effect if Enemy Randomizer is disabled."""
|
|
||||||
|
Has no effect if Enemy Randomizer is disabled.
|
||||||
|
"""
|
||||||
display_name = "Enemy Groups"
|
display_name = "Enemy Groups"
|
||||||
|
|
||||||
|
|
||||||
class EnemyScaling(DefaultOnToggle):
|
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"
|
display_name = "Enemy Scaling"
|
||||||
|
|
||||||
|
|
||||||
class BlasphemousDeathLink(DeathLink):
|
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 = {
|
@dataclass
|
||||||
"prie_dieu_warp": PrieDieuWarp,
|
class BlasphemousOptions(PerGameCommonOptions):
|
||||||
"skip_cutscenes": SkipCutscenes,
|
prie_dieu_warp: PrieDieuWarp
|
||||||
"corpse_hints": CorpseHints,
|
skip_cutscenes: SkipCutscenes
|
||||||
"difficulty": Difficulty,
|
corpse_hints: CorpseHints
|
||||||
"penitence": Penitence,
|
difficulty: Difficulty
|
||||||
"starting_location": StartingLocation,
|
penitence: Penitence
|
||||||
"ending": Ending,
|
starting_location: StartingLocation
|
||||||
"skip_long_quests": SkipLongQuests,
|
ending: Ending
|
||||||
"thorn_shuffle" : ThornShuffle,
|
skip_long_quests: SkipLongQuests
|
||||||
"dash_shuffle": DashShuffle,
|
thorn_shuffle: ThornShuffle
|
||||||
"wall_climb_shuffle": WallClimbShuffle,
|
dash_shuffle: DashShuffle
|
||||||
"reliquary_shuffle": ReliquaryShuffle,
|
wall_climb_shuffle: WallClimbShuffle
|
||||||
"boots_of_pleading": CustomItem1,
|
reliquary_shuffle: ReliquaryShuffle
|
||||||
"purified_hand": CustomItem2,
|
boots_of_pleading: CustomItem1
|
||||||
"start_wheel": StartWheel,
|
purified_hand: CustomItem2
|
||||||
"skill_randomizer": SkillRando,
|
start_wheel: StartWheel
|
||||||
"enemy_randomizer": EnemyRando,
|
skill_randomizer: SkillRando
|
||||||
"enemy_groups": EnemyGroups,
|
enemy_randomizer: EnemyRando
|
||||||
"enemy_scaling": EnemyScaling,
|
enemy_groups: EnemyGroups
|
||||||
"death_link": BlasphemousDeathLink,
|
enemy_scaling: EnemyScaling
|
||||||
"start_inventory": StartInventoryPool
|
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
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
|
@ -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
|
@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
junk_locations: Set[str] = [
|
junk_locations: Set[str] = {
|
||||||
"Albero: Donate 50000 Tears",
|
"Albero: Donate 50000 Tears",
|
||||||
"Ossuary: 11th reward",
|
"Ossuary: 11th reward",
|
||||||
"AtTotS: Miriam's gift",
|
"AtTotS: Miriam's gift",
|
||||||
"TSC: Jocinero's final reward"
|
"TSC: Jocinero's final reward"
|
||||||
]
|
}
|
||||||
|
|
||||||
|
|
||||||
thorn_set: Set[str] = {
|
thorn_set: Set[str] = {
|
||||||
|
@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
|
||||||
"Skill 5, Tier 1": "Lunge Skill",
|
"Skill 5, Tier 1": "Lunge Skill",
|
||||||
"Skill 5, Tier 2": "Lunge Skill",
|
"Skill 5, Tier 2": "Lunge Skill",
|
||||||
"Skill 5, Tier 3": "Lunge Skill",
|
"Skill 5, Tier 3": "Lunge Skill",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from typing import Dict, List, Set, Any
|
from typing import Dict, List, Set, Any
|
||||||
from collections import Counter
|
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 worlds.AutoWorld import World, WebWorld
|
||||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
|
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
||||||
from .Locations import location_table
|
from .Locations import location_names
|
||||||
from .Rooms import room_table, door_table
|
from .Rules import BlasRules
|
||||||
from .Rules import rules
|
from worlds.generic.Rules import set_rule
|
||||||
from worlds.generic.Rules import set_rule, add_rule
|
from .Options import BlasphemousOptions, blas_option_groups
|
||||||
from .Options import blasphemous_options
|
|
||||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||||
|
from .region_data import regions, locations
|
||||||
|
|
||||||
class BlasphemousWeb(WebWorld):
|
class BlasphemousWeb(WebWorld):
|
||||||
theme = "stone"
|
theme = "stone"
|
||||||
|
@ -21,39 +21,33 @@ class BlasphemousWeb(WebWorld):
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["TRPG"]
|
["TRPG"]
|
||||||
)]
|
)]
|
||||||
|
option_groups = blas_option_groups
|
||||||
|
|
||||||
|
|
||||||
class BlasphemousWorld(World):
|
class BlasphemousWorld(World):
|
||||||
"""
|
"""
|
||||||
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
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!
|
your eternal damnation!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
game: str = "Blasphemous"
|
game = "Blasphemous"
|
||||||
web = BlasphemousWeb()
|
web = BlasphemousWeb()
|
||||||
|
|
||||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
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_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
|
||||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
|
||||||
|
|
||||||
item_name_groups = group_table
|
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):
|
def __init__(self, multiworld, player):
|
||||||
super(BlasphemousWorld, self).__init__(multiworld, player)
|
super(BlasphemousWorld, self).__init__(multiworld, player)
|
||||||
self.start_room: str = "D17Z01S01"
|
self.start_room: str = "D17Z01S01"
|
||||||
self.door_connections: Dict[str, str] = {}
|
self.disabled_locations: List[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)
|
|
||||||
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> "BlasphemousItem":
|
def create_item(self, name: str) -> "BlasphemousItem":
|
||||||
|
@ -68,64 +62,56 @@ class BlasphemousWorld(World):
|
||||||
|
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.multiworld.random.choice(tears_set)
|
return self.random.choice(tears_list)
|
||||||
|
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
world = self.multiworld
|
if not self.options.starting_location.randomized:
|
||||||
player = self.player
|
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 (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
|
||||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
and self.options.dash_shuffle:
|
||||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||||
" cannot be chosen if Difficulty is lower than Hard.")
|
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
|
||||||
|
|
||||||
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 world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
|
||||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||||
else:
|
else:
|
||||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
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)
|
locations.remove(6)
|
||||||
|
|
||||||
if world.dash_shuffle[player]:
|
if self.options.dash_shuffle:
|
||||||
locations.remove(0)
|
locations.remove(0)
|
||||||
if 6 in locations:
|
if 6 in locations:
|
||||||
locations.remove(6)
|
locations.remove(6)
|
||||||
|
|
||||||
if world.wall_climb_shuffle[player]:
|
if self.options.wall_climb_shuffle:
|
||||||
locations.remove(3)
|
locations.remove(3)
|
||||||
|
|
||||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
if self.options.starting_location.value not in locations:
|
||||||
invalid = True
|
self.options.starting_location.value = self.random.choice(locations)
|
||||||
|
|
||||||
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 not world.dash_shuffle[player]:
|
if not self.options.dash_shuffle:
|
||||||
world.push_precollected(self.create_item("Dash Ability"))
|
self.multiworld.push_precollected(self.create_item("Dash Ability"))
|
||||||
|
|
||||||
if not world.wall_climb_shuffle[player]:
|
if not self.options.wall_climb_shuffle:
|
||||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
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:
|
for loc in junk_locations:
|
||||||
world.exclude_locations[player].value.add(loc)
|
self.options.exclude_locations.value.add(loc)
|
||||||
|
|
||||||
start_rooms: Dict[int, str] = {
|
start_rooms: Dict[int, str] = {
|
||||||
0: "D17Z01S01",
|
0: "D17Z01S01",
|
||||||
|
@ -137,13 +123,10 @@ class BlasphemousWorld(World):
|
||||||
6: "D20Z02S09"
|
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):
|
def create_items(self):
|
||||||
world = self.multiworld
|
|
||||||
player = self.player
|
|
||||||
|
|
||||||
removed: int = 0
|
removed: int = 0
|
||||||
to_remove: List[str] = [
|
to_remove: List[str] = [
|
||||||
"Tears of Atonement (250)",
|
"Tears of Atonement (250)",
|
||||||
|
@ -156,46 +139,46 @@ class BlasphemousWorld(World):
|
||||||
skipped_items = []
|
skipped_items = []
|
||||||
junk: int = 0
|
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):
|
for _ in range(count):
|
||||||
skipped_items.append(item)
|
skipped_items.append(item)
|
||||||
junk += 1
|
junk += 1
|
||||||
|
|
||||||
skipped_items.extend(unrandomized_dict.values())
|
skipped_items.extend(unrandomized_dict.values())
|
||||||
|
|
||||||
if world.thorn_shuffle[player] == 2:
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
for i in range(8):
|
for _ in range(8):
|
||||||
skipped_items.append("Thorn Upgrade")
|
skipped_items.append("Thorn Upgrade")
|
||||||
|
|
||||||
if world.dash_shuffle[player]:
|
if self.options.dash_shuffle:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not world.dash_shuffle[player]:
|
elif not self.options.dash_shuffle:
|
||||||
skipped_items.append("Dash Ability")
|
skipped_items.append("Dash Ability")
|
||||||
|
|
||||||
if world.wall_climb_shuffle[player]:
|
if self.options.wall_climb_shuffle:
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
elif not world.wall_climb_shuffle[player]:
|
elif not self.options.wall_climb_shuffle:
|
||||||
skipped_items.append("Wall Climb Ability")
|
skipped_items.append("Wall Climb Ability")
|
||||||
|
|
||||||
if not world.reliquary_shuffle[player]:
|
if not self.options.reliquary_shuffle:
|
||||||
skipped_items.extend(reliquary_set)
|
skipped_items.extend(reliquary_set)
|
||||||
elif world.reliquary_shuffle[player]:
|
elif self.options.reliquary_shuffle:
|
||||||
for i in range(3):
|
for _ in range(3):
|
||||||
skipped_items.append(to_remove[removed])
|
skipped_items.append(to_remove[removed])
|
||||||
removed += 1
|
removed += 1
|
||||||
|
|
||||||
if not world.boots_of_pleading[player]:
|
if not self.options.boots_of_pleading:
|
||||||
skipped_items.append("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")
|
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")
|
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())
|
skipped_items.extend(skill_dict.values())
|
||||||
|
|
||||||
counter = Counter(skipped_items)
|
counter = Counter(skipped_items)
|
||||||
|
@ -208,184 +191,140 @@ class BlasphemousWorld(World):
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
for i in range(count):
|
for _ in range(count):
|
||||||
pool.append(self.create_item(item["name"]))
|
pool.append(self.create_item(item["name"]))
|
||||||
|
|
||||||
for _ in range(junk):
|
for _ in range(junk):
|
||||||
pool.append(self.create_item(self.get_filler_item_name()))
|
pool.append(self.create_item(self.get_filler_item_name()))
|
||||||
|
|
||||||
world.itempool += pool
|
self.multiworld.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
def pre_fill(self):
|
def pre_fill(self):
|
||||||
world = self.multiworld
|
|
||||||
player = self.player
|
|
||||||
|
|
||||||
self.place_items_from_dict(unrandomized_dict)
|
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")
|
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||||
|
|
||||||
if world.start_wheel[player]:
|
if self.options.start_wheel:
|
||||||
world.get_location("Beginning gift", player)\
|
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||||
.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)
|
self.place_items_from_dict(skill_dict)
|
||||||
|
|
||||||
if world.thorn_shuffle[player] == 1:
|
if self.options.thorn_shuffle == "local_only":
|
||||||
world.local_items[player].value.add("Thorn Upgrade")
|
self.options.local_items.value.add("Thorn Upgrade")
|
||||||
|
|
||||||
|
|
||||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||||
for loc in location_set:
|
for loc in location_set:
|
||||||
self.multiworld.get_location(loc, self.player)\
|
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||||
.place_locked_item(self.create_item(name))
|
|
||||||
|
|
||||||
|
|
||||||
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
||||||
for loc, item in option_dict.items():
|
for loc, item in option_dict.items():
|
||||||
self.multiworld.get_location(loc, self.player)\
|
self.get_location(loc).place_locked_item(self.create_item(item))
|
||||||
.place_locked_item(self.create_item(item))
|
|
||||||
|
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
|
multiworld = self.multiworld
|
||||||
player = self.player
|
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)
|
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
|
||||||
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.place_locked_item(self.create_event("Victory"))
|
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))
|
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
|
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||||
state.has("Holy Wound of Abnegation", player))
|
state.has("Holy Wound of Abnegation", player))
|
||||||
|
|
||||||
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
multiworld.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)
|
|
||||||
|
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, Any]:
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
slot_data: Dict[str, Any] = {}
|
slot_data: Dict[str, Any] = {}
|
||||||
locations = []
|
|
||||||
doors: Dict[str, str] = {}
|
doors: Dict[str, str] = {}
|
||||||
|
|
||||||
world = self.multiworld
|
|
||||||
player = self.player
|
|
||||||
thorns: bool = True
|
thorns: bool = True
|
||||||
|
|
||||||
if world.thorn_shuffle[player].value == 2:
|
if self.options.thorn_shuffle == "vanilla":
|
||||||
thorns = False
|
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 = {
|
config = {
|
||||||
"LogicDifficulty": world.difficulty[player].value,
|
"LogicDifficulty": self.options.difficulty.value,
|
||||||
"StartingLocation": world.starting_location[player].value,
|
"StartingLocation": self.options.starting_location.value,
|
||||||
"VersionCreated": "AP",
|
"VersionCreated": "AP",
|
||||||
|
|
||||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
|
||||||
"AllowHints": bool(world.corpse_hints[player].value),
|
"AllowHints": bool(self.options.corpse_hints.value),
|
||||||
"AllowPenitence": bool(world.penitence[player].value),
|
"AllowPenitence": bool(self.options.penitence.value),
|
||||||
|
|
||||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
|
||||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
|
||||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
|
||||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
"ShuffleDash": bool(self.options.dash_shuffle.value),
|
||||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].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,
|
"ShuffleThorns": thorns,
|
||||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
"JunkLongQuests": bool(self.options.skip_long_quests.value),
|
||||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
"StartWithWheel": bool(self.options.start_wheel.value),
|
||||||
|
|
||||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
"EnemyShuffleType": self.options.enemy_randomizer.value,
|
||||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
"MaintainClass": bool(self.options.enemy_groups.value),
|
||||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
"AreaScaling": bool(self.options.enemy_scaling.value),
|
||||||
|
|
||||||
"BossShuffleType": 0,
|
"BossShuffleType": 0,
|
||||||
"DoorShuffleType": 0
|
"DoorShuffleType": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
slot_data = {
|
slot_data = {
|
||||||
"locations": locations,
|
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
|
||||||
"doors": doors,
|
"doors": doors,
|
||||||
"cfg": config,
|
"cfg": config,
|
||||||
"ending": world.ending[self.player].value,
|
"ending": self.options.ending.value,
|
||||||
"death_link": bool(world.death_link[self.player].value)
|
"death_link": bool(self.options.death_link.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
|
@ -1,48 +1,17 @@
|
||||||
# Blasphemous Multiworld Setup Guide
|
# 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:
|
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
|
||||||
- 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)
|
|
||||||
|
|
||||||
Optional:
|
Some optional mods are also recommended:
|
||||||
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
|
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
|
||||||
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
|
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
|
||||||
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
|
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
|
||||||
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
|
|
||||||
|
|
||||||
## 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),
|
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
|
||||||
and point it to your install directory for Blasphemous.
|
- `ap status` - Display connection status.
|
||||||
|
- `ap say [message]` - Send a message to the server.
|
||||||
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
|
- `ap hint [item]` - Request a hint for an item from the server.
|
||||||
`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.**
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
||||||
|
from test.bases import WorldTestBase
|
||||||
|
from .. import BlasphemousWorld
|
||||||
|
|
||||||
|
|
||||||
|
class BlasphemousTestBase(WorldTestBase):
|
||||||
|
game = "Blasphemous"
|
||||||
|
world: BlasphemousWorld
|
|
@ -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)
|
|
@ -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"
|
||||||
|
}
|
Loading…
Reference in New Issue