diff --git a/worlds/diablo2/Events.py b/worlds/diablo2/Events.py index 2c6af029fdd..419f04c0181 100644 --- a/worlds/diablo2/Events.py +++ b/worlds/diablo2/Events.py @@ -12,9 +12,7 @@ def create_location(player: int, name: str, region: Region) -> Location: return Locations.D2Location(player, name, None, region) -def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location: - region = multiworld.get_region(region_name, player) - +def create_locked_location_event(player: int, region: Region, item: str) -> Location: new_location = create_location(player, item, region) new_location.place_locked_item(create_event(player, item)) @@ -22,11 +20,12 @@ def create_locked_location_event(multiworld: MultiWorld, player: int, region_nam return new_location -def create_all_events(multiworld: MultiWorld, player: int) -> None: - for region, event in event_locks.items(): - create_locked_location_event(multiworld, player, region, event) +def create_all_events(world: "Diablo2World", regions: Dict[str, Region]) -> None: + for region_name, event in event_locks.items(): + region = regions[region_name] + create_locked_location_event(world.player, region, event) - multiworld.completion_condition[player] = lambda state: state.has("Victory", player) + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) # Maps region names to event names diff --git a/worlds/diablo2/Items.py b/worlds/diablo2/Items.py index 4980c4f8360..e6dc2dcea11 100644 --- a/worlds/diablo2/Items.py +++ b/worlds/diablo2/Items.py @@ -27,32 +27,27 @@ def create_fixed_item_pool() -> List[str]: return list(Counter(required_items).elements()) -def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: +def create_random_items(world: "Diablo2World", random_count: int) -> List[str]: filler_pool = filler_weights.copy() - if multiworld.traps[player].value == 0: + if world.options.traps.value == 0: del filler_pool["Trap"] - return multiworld.random.choices( + return world.random.choices( population=list(filler_pool.keys()), weights=list(filler_pool.values()), k=random_count ) -def create_all_items(multiworld: MultiWorld, player: int) -> None: - sum_locations = len(multiworld.get_unfilled_locations(player)) +def create_all_items(world: "Diablo2World") -> None: + sum_locations = len(world.multiworld.get_unfilled_locations(world.player)) - itempool = ( - create_fixed_item_pool() - + create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player]) - + create_spatial_awareness_item(multiworld.bosses_as_checks[player]) - + create_kantele(multiworld.victory_condition[player]) - ) + itempool = create_fixed_item_pool() random_count = sum_locations - len(itempool) - itempool += create_random_items(multiworld, player, random_count) + itempool += create_random_items(world, random_count) - multiworld.itempool += [create_item(player, name) for name in itempool] + multiworld.itempool += [create_item(world.player, name) for name in itempool] # Total: 33 diff --git a/worlds/diablo2/Locations.py b/worlds/diablo2/Locations.py index 073b7b600a9..04b7e94de30 100644 --- a/worlds/diablo2/Locations.py +++ b/worlds/diablo2/Locations.py @@ -360,11 +360,6 @@ class LocationData(NamedTuple): } -# Iterating the hidden chest and pedestal locations here to avoid clutter above -def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]: - return {locname: locinfo.id} - - location_name_groups: Dict[str, Set[str]] = { "Act 1": set(), "Act 2": set(), "Act 3": set(), "Act 4": set(), "Act 5": set() } @@ -373,5 +368,5 @@ def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, for location_group in location_region_mapping.values(): for locname, locinfo in location_group.items(): - location_name_to_id.update(generate_location_entries(locname, locinfo)) + location_name_to_id[locname] = locinfo.id location_name_groups[locinfo.group].add(locname) diff --git a/worlds/diablo2/Options.py b/worlds/diablo2/Options.py index b5e9c673e82..9b6572f80c1 100644 --- a/worlds/diablo2/Options.py +++ b/worlds/diablo2/Options.py @@ -1,8 +1,8 @@ -from typing import Dict -from Options import AssembleOptions, DeathLink, DefaultOnToggle, Toggle +from Options import AssembleOptions, DeathLink, DefaultOnToggle, PerGameCommonOptions, Toggle +from dataclasses import dataclass -class Traps(DefaultOnToggle): +class Traps(Toggle): """Whether negative effects that can hinder your character are added to the item pool.""" display_name = "Traps" @@ -22,10 +22,16 @@ class GoldenChestsAsChecks(Toggle): display_name = "Golden Chests as Checks" -diablo2_options: Dict[str, AssembleOptions] = { - "death_link": DeathLink, - "traps": Traps, - "waypoints_as_checks": WaypointsAsChecks, - "superuniques_as_checks": SuperuniquesAsChecks, - "goldenchests_as_checks": GoldenChestsAsChecks, -} +class IsLordOfDestruction(DefaultOnToggle): + """Includes the Diablo II: Lord of Destruction expansion content.""" + display_name = "Is Expansion" + + +@dataclass +class Diablo2Options(PerGameCommonOptions): + death_link: DeathLink + traps: Traps + waypoints_as_checks: WaypointsAsChecks + superuniques_as_checks: SuperuniquesAsChecks + goldenchests_as_checks: GoldenChestsAsChecks + is_expansion: IsLordOfDestruction diff --git a/worlds/diablo2/Regions.py b/worlds/diablo2/Regions.py index 310a030b109..c2bdd24cd4f 100644 --- a/worlds/diablo2/Regions.py +++ b/worlds/diablo2/Regions.py @@ -2,47 +2,49 @@ from typing import Dict, Set from BaseClasses import Entrance, MultiWorld, Region -from . import Locations +from . import Events, Locations -def add_location(player: int, loc_name: str, id: int, region: Region) -> None: - location = Locations.NoitaLocation(player, loc_name, id, region) - region.locations.append(location) - - -def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None: +def add_locations(world: "Diablo2World", region: Region) -> None: locations = Locations.location_region_mapping.get(region.name, {}) for location_name, location_data in locations.items(): location_type = location_data.ltype - flag = location_data.flag - - opt_orbs = multiworld.orbs_as_checks[player].value - opt_bosses = multiworld.bosses_as_checks[player].value - opt_paths = multiworld.path_option[player].value - opt_num_chests = multiworld.hidden_chests[player].value - opt_num_pedestals = multiworld.pedestal_checks[player].value - - is_orb_allowed = location_type == "orb" and flag <= opt_orbs - is_boss_allowed = location_type == "boss" and flag <= opt_bosses - if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: - add_location(player, location_name, location_data.id, region) - elif location_type == "chest" and flag <= opt_paths: - for i in range(opt_num_chests): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) - elif location_type == "pedestal" and flag <= opt_paths: - for i in range(opt_num_pedestals): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) + + match location_type: + case Locations.LocType.waypoint: + if not world.options.waypoints_as_checks: continue + case Locations.LocType.goldenchest: + if not world.options.goldenchests_as_checks: continue + case Locations.LocType.superunique: + if not world.options.superuniques_as_checks: continue + + if not world.options.is_expansion and location_data.group == "Act 5": + continue + + region.add_locations({location_name: location_data.id}) # Creates a new Region with the locations found in `location_region_mapping` and adds them to the world. -def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region: - new_region = Region(region_name, player, multiworld) - add_locations(multiworld, player, new_region) +def create_region(world: "Diablo2World", region_name: str) -> Region: + new_region = Region(region_name, world.player, world.multiworld) + add_locations(world, new_region) return new_region -def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]: - return {name: create_region(multiworld, player, name) for name in noita_regions} +def get_connection_data(world: "Diablo2World") -> Dict[str, Set[str]]: + if world.options.is_expansion: + return d2_expansion_connections + return d2_connections + + +def get_region_data(world: "Diablo2World") -> Set[str]: + if world.options.is_expansion: + return d2_expansion_regions + return d2_regions + + +def create_regions(world: "Diablo2World") -> Dict[str, Region]: + return {name: create_region(world, name) for name in get_region_data(world)} # An "Entrance" is really just a connection between two regions @@ -52,135 +54,94 @@ def create_entrance(player: int, source: str, destination: str, regions: Dict[st return entrance -# Creates connections based on our access mapping in `noita_connections`. -def create_connections(player: int, regions: Dict[str, Region]) -> None: - for source, destinations in noita_connections.items(): - new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations] +# Creates connections based on our access mapping in `d2_connections`. +def create_connections(world: "Diablo2World", regions: Dict[str, Region]) -> None: + for source, destinations in get_connection_data(world).items(): + new_entrances = [create_entrance(world.player, source, destination, regions) for destination in destinations] regions[source].exits = new_entrances -# Creates all regions and connections. Called from NoitaWorld. -def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None: - created_regions = create_regions(multiworld, player) - create_connections(player, created_regions) +# Creates all regions and connections. Called from Diablo2World. +def create_all_regions_and_connections(world: "Diablo2World") -> None: + created_regions = create_regions(world) + create_connections(world, created_regions) + Events.create_all_events(world, created_regions) - multiworld.regions += created_regions.values() + world.multiworld.regions += created_regions.values() -# Oh, what a tangled web we weave -# Notes to create artificial spheres: -# - Shaft is excluded to disconnect Mines from the Snowy Depths -# - Lukki Lair is disconnected from The Vault -# - Overgrown Cavern is connected to the Underground Jungle instead of the Desert due to similar difficulty -# - Powerplant is disconnected from the Sandcave due to difficulty and sphere creation -# - Snow Chasm is disconnected from the Snowy Wasteland -# - Pyramid is connected to the Hiisi Base instead of the Desert due to similar difficulty -# - Frozen Vault is connected to the Vault instead of the Snowy Wasteland due to similar difficulty -# - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game) -# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable -# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1 -noita_connections: Dict[str, Set[str]] = { +d2_connections: Dict[str, Set[str]] = { # Act 1 "Menu": {"Rogue Encampment"}, "Rogue Encampment": {"Blood Moor", "Lut Gholein", "Secret Cow Level"}, - "Secret Cow Level": {"Rogue Encampment"}, - "Blood Moor": {"Rogue Encampment", "Cold Plains", "Den of Evil"}, - "Den of Evil": {"Blood Moor"}, - "Cold Plains": {"Blood Moor", "Burial Grounds", "Stony Field", "The Cave"}, - "The Cave": {"Cold Plains"}, - "Burial Grounds": {"Cold Plains", "The Mausoleum", "The Crypt"}, - "The Mausoleum": {"Burial Grounds"}, - "The Crypt": {"Burial Grounds"}, - "Stony Field": {"Cold Plains", "Underground Passage"}, - "Underground Passage": {"Stony Field", "Dark Wood"}, - "Dark Wood": {"Underground Passage", "Black Marsh"}, - "Black Marsh": {"Dark Wood", "Tamoe Highland", "The Forgotten Tower", "The Hole"}, - "The Hole": {"Black Marsh"}, - "The Forgotten Tower": {"Black Marsh"}, - "Tamoe Highland": {"Black Marsh", "The Pit", "Monestary Gate"}, - "The Pit": {"Tamoe Highland"}, - "Monestary Gate": {"Tamoe Highland", "Outer Cloister"}, - "Outer Cloister": {"Monestary Gate", "Barracks"}, - "Barracks": {"Outer Cloister", "Jail"}, - "Jail": {"Barracks", "Inner Cloister"}, - "Inner Cloister": {"Jail", "Cathedral"}, - "Cathedral": {"Inner Cloister", "Catacombs"}, - "Catacombs": {"Cathedral"}, + "Blood Moor": {"Cold Plains", "Den of Evil"}, + "Cold Plains": {"Burial Grounds", "Stony Field", "The Cave"}, + "Burial Grounds": {"The Mausoleum", "The Crypt"}, + "Stony Field": {"Underground Passage", "Tristram"}, + "Underground Passage": {"Dark Wood"}, + "Dark Wood": {"Black Marsh"}, + "Black Marsh": {"Tamoe Highland", "The Forgotten Tower", "The Hole"}, + "Tamoe Highland": {"The Pit", "Monestary Gate"}, + "Monestary Gate": {"Outer Cloister"}, + "Outer Cloister": {"Barracks"}, + "Barracks": {"Jail"}, + "Jail": {"Inner Cloister"}, + "Inner Cloister": {"Cathedral"}, + "Cathedral": {"Catacombs"}, # Act 2 - "Lut Gholein": {"Lut Gholein Sewers", "Rocky Waste", "Harem", "Rogue Encampment", "Kurast Docks"}, - "Lut Gholein Sewers": {"Lut Gholein"}, - "Rocky Waste": {"Lut Gholein", "The Stony Tomb", "Dry Hills"}, - "The Stony Tomb": {"Rocky Waste"}, - "Dry Hills": {"Rocky Waste", "Halls of the Dead", "Far Oasis"}, - "Halls of the Dead": {"Dry Hills"}, - "Far Oasis": {"Dry Hills", "Maggot Lair", "Lost City"}, - "Maggot Lair": {"Far Oasis"}, - "Lost City": {"Far Oasis", "The Ancient Tunnels", "The Valley of Snakes"}, - "The Ancient Tunnels": {"Lost City"}, - "The Valley of Snakes": {"Lost City", "Claw Viper Temple"}, - "Claw Viper Temple": {"The Valley of Snakes"}, - "Harem": {"Lut Gholein", "Palace Cellar"}, - "Palace Cellar": {"Harem", "Arcane Sanctuary"}, - "Arcane Sanctuary": {"Palace Cellar", "Canyon of the Magi"}, + "Lut Gholein": {"Lut Gholein Sewers", "Rocky Waste", "Harem", "Kurast Docks"}, + "Rocky Waste": {"The Stony Tomb", "Dry Hills"}, + "Dry Hills": {"Halls of the Dead", "Far Oasis"}, + "Far Oasis": {"Maggot Lair", "Lost City"}, + "Lost City": {"The Ancient Tunnels", "The Valley of Snakes"}, + "The Valley of Snakes": {"Claw Viper Temple"}, + "Harem": {"Palace Cellar"}, + "Palace Cellar": {"Arcane Sanctuary"}, + "Arcane Sanctuary": {"Canyon of the Magi"}, "Canyon of the Magi": {"Tal Rasha's Tomb"}, - "Tal Rasha's Tomb": {"Canyon of the Magi", "Tal Rasha's Chamber"}, - "Tal Rasha's Chamber": {"Lut Gholein"}, + "Tal Rasha's Tomb": {"Tal Rasha's Chamber"}, # Act 3 - "Kurast Docks": {"Lut Gholein", "Spider Forest"}, - "Spider Forest": {"Kurast Docks", "Spider Cavern", "Arachnid Lair", "Great Marsh", "Flayer Jungle"}, - "Spider Cavern": {"Spider Forest"}, - "Arachnid Lair": {"Spider Forest"}, - "Great Marsh": {"Spider Forest", "Flayer Jungle"}, - "Flayer Jungle": {"Spider Forest", "Great Marsh", "Flayer Dungeon", "Swampy Pit", "Lower Kurast"}, - "Flayer Dungeon": {"Flayer Jungle"}, - "Swampy Pit": {"Flayer Jungle"}, - "Lower Kurast": {"Flayer Jungle", "Kurast Bazaar"}, - "Kurast Bazaar": {"Lower Kurast", "Ruined Temple", "Disused Fane", "Kurast Sewers", "Upper Kurast"}, - "Ruined Temple": {"Kurast Bazaar"}, - "Disused Fane": {"Kurast Bazaar"}, - "Kurast Sewers": {"Kurast Bazaar", "Upper Kurast"}, - "Upper Kurast": {"Kurast Bazaar", "Kurast Sewers", "Forgotten Temple", "Forgotten Reliquary", "Kurast Causeway"}, - "Forgotten Temple": {"Upper Kurast"}, - "Forgotten Reliquary": {"Upper Kurast"}, - "Kurast Causeway": {"Upper Kurast", "Disused Reliquary", "Ruined Fane", "Travincal"}, - "Disused Reliquary": {"Kurast Causeway"}, - "Ruined Fane": {"Kurast Causeway"}, - "Travincal": {"Kurast Causeway", "Durance of Hate"}, - "Durance of Hate": {"Travincal", "Pandemonium Fortress"}, + "Kurast Docks": {"Spider Forest"}, + "Spider Forest": {"Spider Cavern", "Arachnid Lair", "Great Marsh", "Flayer Jungle"}, + "Great Marsh": {"Flayer Jungle"}, + "Flayer Jungle": {"Great Marsh", "Flayer Dungeon", "Swampy Pit", "Lower Kurast"}, + "Lower Kurast": {"Kurast Bazaar"}, + "Kurast Bazaar": {"Ruined Temple", "Disused Fane", "Kurast Sewers", "Upper Kurast"}, + "Upper Kurast": {"Kurast Sewers", "Forgotten Temple", "Forgotten Reliquary", "Kurast Causeway"}, + "Kurast Causeway": {"Disused Reliquary", "Ruined Fane", "Travincal"}, + "Travincal": {"Durance of Hate"}, + "Durance of Hate": {"Pandemonium Fortress"}, + + # Act 4 + "Pandemonium Fortress": {"Harrogath", "Outer Steppes"}, + "Outer Steppes": {"Plains of Despair"}, + "Plains of Despair": {"City of the Damned"}, + "City of the Damned": {"River of Flame"}, + "River of Flame": {"Chaos Sanctuary"}, +} +d2_expansion_connections: Dict[str, Set[str]] = {**d2_connections, # Act 4 "Pandemonium Fortress": {"Harrogath", "Outer Steppes"}, - "Outer Steppes": {"Pandemonium Fortress", "Plains of Despair"}, - "Plains of Despair": {"Outer Steppes", "City of the Damned"}, - "City of the Damned": {"Plains of Despair", "River of Flame"}, - "River of Flame": {"City of the Damned", "Chaos Sanctuary"}, - "Chaos Sanctuary": {"River of Flame"}, # Act 5 "Harrogath": {"Bloody Foothills", "Nihlathak's Temple"}, - "Nihlathak's Temple": {"Harrogath", "Halls of Anguish"}, - "Halls of Anguish": {"Nihlathak's Temple", "Halls of Pain"}, - "Halls of Pain": {"Halls of Anguish", "Halls of Vaught"}, - "Halls of Vaught": {"Halls of Pain"}, - "Bloody Foothills": {"Harrogath", "Frigid Highlands"}, - "Frigid Highlands": {"Bloody Foothills", "Abaddon", "Arreat Plateau"}, - "Abaddon": {"Frigid Highlands"}, - "Arreat Plateau": {"Frigid Highlands", "Pit of Acheron", "Crystalline Passage"}, - "Pit of Acheron": {"Arreat Plateau"}, - "Crystalline Passage": {"Arreat Plateau", "Frozen River", "Glacial Trail"}, - "Frozen River": {"Crystalline Passage"}, - "Glacial Trail": {"Crystalline Passage", "Drifter Cavern", "Frozen Tundra"}, - "Drifter Cavern": {"Glacial Trail"}, - "Frozen Tundra": {"Glacial Trail", "Infernal Pit", "The Ancients' Way"}, - "Infernal Pit": {"Frozen Tundra"}, + "Nihlathak's Temple": {"Halls of Anguish"}, + "Halls of Anguish": {"Halls of Pain"}, + "Halls of Pain": {"Halls of Vaught"}, + "Bloody Foothills": {"Frigid Highlands"}, + "Frigid Highlands": {"Abaddon", "Arreat Plateau"}, + "Arreat Plateau": {"Pit of Acheron", "Crystalline Passage"}, + "Crystalline Passage": {"Frozen River", "Glacial Trail"}, + "Glacial Trail": {"Drifter Cavern", "Frozen Tundra"}, + "Frozen Tundra": {"Infernal Pit", "The Ancients' Way"}, "The Ancients' Way": {"Icy Cellar", "Arreat Summit"}, - "Icy Cellar": {"The Ancients' Way"}, "Arreat Summit": {"Worldstone Keep"}, - "Worldstone Keep": {"Arreat Summit", "Throne of Destruction"}, - "Throne of Destruction": {"Worldstone Keep", "Worldstone Chamber"}, - "Worldstone Chamber": {}, + "Worldstone Keep": {"Throne of Destruction"}, + "Throne of Destruction": {"Worldstone Chamber"}, } -noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) +d2_regions: Set[str] = set(d2_connections.keys()).union(*d2_connections.values()) +d2_expansion_regions: Set[str] = set(d2_expansion_connections.keys()).union(*d2_expansion_connections.values()) diff --git a/worlds/diablo2/Rules.py b/worlds/diablo2/Rules.py index 3eb6be5a7c6..06dcccb1aec 100644 --- a/worlds/diablo2/Rules.py +++ b/worlds/diablo2/Rules.py @@ -154,14 +154,5 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: # ---------------- -def create_all_rules(multiworld: MultiWorld, player: int) -> None: - ban_items_from_shops(multiworld, player) - ban_early_high_tier_wands(multiworld, player) - lock_holy_mountains_into_spheres(multiworld, player) - holy_mountain_unlock_conditions(multiworld, player) - biome_unlock_conditions(multiworld, player) - victory_unlock_conditions(multiworld, player) - - # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) - if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses: - forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player) +def create_all_rules(world: "Diablo2World") -> None: + "TODO" diff --git a/worlds/diablo2/__init__.py b/worlds/diablo2/__init__.py index 8acdcb31fd8..2a50013f1a1 100644 --- a/worlds/diablo2/__init__.py +++ b/worlds/diablo2/__init__.py @@ -1,6 +1,7 @@ from BaseClasses import Item, Tutorial from worlds.AutoWorld import WebWorld, World -from . import Events, Items, Locations, Options, Regions, Rules +from . import Events, Items, Locations, Regions, Rules +from .Options import Diablo2Options class Diablo2Web(WebWorld): @@ -23,7 +24,8 @@ class Diablo2World(World): """ game = "Diablo II" - option_definitions = Options.diablo2_options + options_dataclass = Diablo2Options + options: Diablo2Options item_name_to_id = Items.item_name_to_id location_name_to_id = Locations.location_name_to_id @@ -36,20 +38,20 @@ class Diablo2World(World): # Returned items will be sent over to the client def fill_slot_data(self): - return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} + return self.options.as_dict("death_link", "waypoints_as_checks", "superuniques_as_checks", + "goldenchests_as_checks") def create_regions(self) -> None: - Regions.create_all_regions_and_connections(self.multiworld, self.player) - Events.create_all_events(self.multiworld, self.player) + Regions.create_all_regions_and_connections(self) def create_item(self, name: str) -> Item: return Items.create_item(self.player, name) def create_items(self) -> None: - Items.create_all_items(self.multiworld, self.player) + Items.create_all_items(self) def set_rules(self) -> None: - Rules.create_all_rules(self.multiworld, self.player) + Rules.create_all_rules(self) def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(Items.filler_items) + return self.random.choice(Items.filler_items)