Files
Grinch-AP/worlds/blasphemous/__init__.py
Trevor L 54a7bb5664 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>
2024-08-21 01:18:28 +02:00

338 lines
12 KiB
Python

from typing import Dict, List, Set, Any
from collections import Counter
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_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"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Blasphemous randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"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 its terrible fate in your quest to break
your eternal damnation!
"""
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: (base_id + index) for index, loc in enumerate(location_names.values())}
item_name_groups = group_table
options_dataclass = BlasphemousOptions
options: BlasphemousOptions
required_client_version = (0, 4, 7)
def __init__(self, multiworld, player):
super(BlasphemousWorld, self).__init__(multiworld, player)
self.start_room: str = "D17Z01S01"
self.disabled_locations: List[str] = []
def create_item(self, name: str) -> "BlasphemousItem":
item_id: int = self.item_name_to_id[name]
id = item_id - base_id
return BlasphemousItem(name, item_table[id]["classification"], item_id, player=self.player)
def create_event(self, event: str):
return BlasphemousItem(event, ItemClassification.progression_skip_balancing, None, self.player)
def get_filler_item_name(self) -> str:
return self.random.choice(tears_list)
def generate_early(self):
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 (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 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 ]
if self.options.difficulty < 2:
locations.remove(6)
if self.options.dash_shuffle:
locations.remove(0)
if 6 in locations:
locations.remove(6)
if self.options.wall_climb_shuffle:
locations.remove(3)
if self.options.starting_location.value not in locations:
self.options.starting_location.value = self.random.choice(locations)
if not self.options.dash_shuffle:
self.multiworld.push_precollected(self.create_item("Dash Ability"))
if not self.options.wall_climb_shuffle:
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
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:
self.options.exclude_locations.value.add(loc)
start_rooms: Dict[int, str] = {
0: "D17Z01S01",
1: "D01Z02S01",
2: "D02Z03S09",
3: "D03Z03S11",
4: "D04Z03S01",
5: "D06Z01S09",
6: "D20Z02S09"
}
self.start_room = start_rooms[self.options.starting_location.value]
def create_items(self):
removed: int = 0
to_remove: List[str] = [
"Tears of Atonement (250)",
"Tears of Atonement (300)",
"Tears of Atonement (500)",
"Tears of Atonement (500)",
"Tears of Atonement (500)"
]
skipped_items = []
junk: int = 0
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 self.options.thorn_shuffle == "vanilla":
for _ in range(8):
skipped_items.append("Thorn Upgrade")
if self.options.dash_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not self.options.dash_shuffle:
skipped_items.append("Dash Ability")
if self.options.wall_climb_shuffle:
skipped_items.append(to_remove[removed])
removed += 1
elif not self.options.wall_climb_shuffle:
skipped_items.append("Wall Climb Ability")
if not self.options.reliquary_shuffle:
skipped_items.extend(reliquary_set)
elif self.options.reliquary_shuffle:
for _ in range(3):
skipped_items.append(to_remove[removed])
removed += 1
if not self.options.boots_of_pleading:
skipped_items.append("Boots of Pleading")
if not self.options.purified_hand:
skipped_items.append("Purified Hand of the Nun")
if self.options.start_wheel:
skipped_items.append("The Young Mason's Wheel")
if not self.options.skill_randomizer:
skipped_items.extend(skill_dict.values())
counter = Counter(skipped_items)
pool = []
for item in item_table:
count = item["count"] - counter[item["name"]]
if count <= 0:
continue
else:
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()))
self.multiworld.itempool += pool
def pre_fill(self):
self.place_items_from_dict(unrandomized_dict)
if self.options.thorn_shuffle == "vanilla":
self.place_items_from_set(thorn_set, "Thorn Upgrade")
if self.options.start_wheel:
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
if not self.options.skill_randomizer:
self.place_items_from_dict(skill_dict)
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.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.get_location(loc).place_locked_item(self.create_item(item))
def create_regions(self) -> None:
multiworld = self.multiworld
player = self.player
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")
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
victory.place_locked_item(self.create_event("Victory"))
self.get_region("D07Z01S03[W]").locations.append(victory)
if self.options.ending == "ending_a":
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
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))
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {}
doors: Dict[str, str] = {}
thorns: bool = True
if self.options.thorn_shuffle == "vanilla":
thorns = False
config = {
"LogicDifficulty": self.options.difficulty.value,
"StartingLocation": self.options.starting_location.value,
"VersionCreated": "AP",
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
"AllowHints": bool(self.options.corpse_hints.value),
"AllowPenitence": bool(self.options.penitence.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(self.options.wall_climb_shuffle.value),
"ShuffleThorns": thorns,
"JunkLongQuests": bool(self.options.skip_long_quests.value),
"StartWithWheel": bool(self.options.start_wheel.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 = {
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
"doors": doors,
"cfg": config,
"ending": self.options.ending.value,
"death_link": bool(self.options.death_link.value)
}
return slot_data
class BlasphemousItem(Item):
game: str = "Blasphemous"
class BlasphemousLocation(Location):
game: str = "Blasphemous"