diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index d8298c85c8fb..9210082f7317 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet +from dataclasses import dataclass class StartingGender(Choice): @@ -336,42 +336,44 @@ class AvailableClasses(OptionSet): The upgraded form of your starting class will be available regardless. """ display_name = "Available Classes" - default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + default = frozenset( + {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + ) valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} -rl_options: Dict[str, type(Option)] = { - "starting_gender": StartingGender, - "starting_class": StartingClass, - "available_classes": AvailableClasses, - "new_game_plus": NewGamePlus, - "fairy_chests_per_zone": FairyChestsPerZone, - "chests_per_zone": ChestsPerZone, - "universal_fairy_chests": UniversalFairyChests, - "universal_chests": UniversalChests, - "vendors": Vendors, - "architect": Architect, - "architect_fee": ArchitectFee, - "disable_charon": DisableCharon, - "require_purchasing": RequirePurchasing, - "progressive_blueprints": ProgressiveBlueprints, - "gold_gain_multiplier": GoldGainMultiplier, - "number_of_children": NumberOfChildren, - "free_diary_on_generation": FreeDiaryOnGeneration, - "khidr": ChallengeBossKhidr, - "alexander": ChallengeBossAlexander, - "leon": ChallengeBossLeon, - "herodotus": ChallengeBossHerodotus, - "health_pool": HealthUpPool, - "mana_pool": ManaUpPool, - "attack_pool": AttackUpPool, - "magic_damage_pool": MagicDamageUpPool, - "armor_pool": ArmorUpPool, - "equip_pool": EquipUpPool, - "crit_chance_pool": CritChanceUpPool, - "crit_damage_pool": CritDamageUpPool, - "allow_default_names": AllowDefaultNames, - "additional_lady_names": AdditionalNames, - "additional_sir_names": AdditionalNames, - "death_link": DeathLink, -} +@dataclass +class RLOptions(PerGameCommonOptions): + starting_gender: StartingGender + starting_class: StartingClass + available_classes: AvailableClasses + new_game_plus: NewGamePlus + fairy_chests_per_zone: FairyChestsPerZone + chests_per_zone: ChestsPerZone + universal_fairy_chests: UniversalFairyChests + universal_chests: UniversalChests + vendors: Vendors + architect: Architect + architect_fee: ArchitectFee + disable_charon: DisableCharon + require_purchasing: RequirePurchasing + progressive_blueprints: ProgressiveBlueprints + gold_gain_multiplier: GoldGainMultiplier + number_of_children: NumberOfChildren + free_diary_on_generation: FreeDiaryOnGeneration + khidr: ChallengeBossKhidr + alexander: ChallengeBossAlexander + leon: ChallengeBossLeon + herodotus: ChallengeBossHerodotus + health_pool: HealthUpPool + mana_pool: ManaUpPool + attack_pool: AttackUpPool + magic_damage_pool: MagicDamageUpPool + armor_pool: ArmorUpPool + equip_pool: EquipUpPool + crit_chance_pool: CritChanceUpPool + crit_damage_pool: CritDamageUpPool + allow_default_names: AllowDefaultNames + additional_lady_names: AdditionalNames + additional_sir_names: AdditionalNames + death_link: DeathLink diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py index 5d07fccbc4d4..61b0ef73ec78 100644 --- a/worlds/rogue_legacy/Regions.py +++ b/worlds/rogue_legacy/Regions.py @@ -1,15 +1,18 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import MultiWorld, Region, Entrance from .Locations import RLLocation, location_table, get_locations_by_category +if TYPE_CHECKING: + from . import RLWorld + class RLRegionData(NamedTuple): locations: Optional[List[str]] region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "RLWorld"): regions: Dict[str, RLRegionData] = { "Menu": RLRegionData(None, ["Castle Hamson"]), "The Manor": RLRegionData([], []), @@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["The Fountain Room"].locations.append("Fountain Room") # Chests - chests = int(multiworld.chests_per_zone[player]) + chests = int(world.options.chests_per_zone) for i in range(0, chests): - if multiworld.universal_chests[player]: + if world.options.universal_chests: regions["Castle Hamson"].locations.append(f"Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") @@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") # Fairy Chests - chests = int(multiworld.fairy_chests_per_zone[player]) + chests = int(world.options.fairy_chests_per_zone) for i in range(0, chests): - if multiworld.universal_fairy_chests[player]: + if world.options.universal_fairy_chests: regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") @@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int): # Set up the regions correctly. for name, data in regions.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) - - multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player)) - multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player)) - multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player)) - multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player)) - multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player)) - multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player)) + world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) + + world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) + world.get_entrance("The Manor").connect(world.get_region("The Manor")) + world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) + world.get_entrance("The Maya").connect(world.get_region("The Maya")) + world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) + world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 2fac8d561399..505bbdd63541 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -1,9 +1,13 @@ -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import RLWorld -def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: - return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \ - int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player]) + +def get_upgrade_total(world: "RLWorld") -> int: + return int(world.options.health_pool) + int(world.options.mana_pool) + \ + int(world.options.attack_pool) + int(world.options.magic_damage_pool) def get_upgrade_count(state: CollectionState, player: int) -> int: @@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool return get_upgrade_count(state, player) >= amount -def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool: - return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100))) +def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: + return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) def has_movement_rune(state: CollectionState, player: int) -> bool: @@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool: return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "RLWorld", player: int): # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if multiworld.vendors[player] == "normal": - multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \ + if world.options.vendors == "normal": + world.get_location("Forest Abkhazia Boss Reward").access_rule = \ lambda state: has_vendors(state, player) # Gate each manor location so everything isn't dumped into sphere 1. manor_rules = { - "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [ + "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ "Manor - Left Wing Window", "Manor - Left Wing Rooftop", "Manor - Right Wing Window", @@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Left Tree 2", "Manor - Right Tree", ], - "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [ + "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ "Manor - Left Big Upper 1", "Manor - Left Big Upper 2", "Manor - Left Big Windows", @@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Right Big Rooftop", "Manor - Right Extension", ], - "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [ + "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ "Manor - Right High Base", "Manor - Right High Upper", "Manor - Right High Tower", @@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int): # Set rules for manor locations. for event, locations in manor_rules.items(): for location in locations: - multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player) + world.get_location(location).access_rule = lambda state: state.has(event, player) # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]: + for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: fairy_location.access_rule = lambda state: has_fairy_progression(state, player) # Region rules. - multiworld.get_entrance("Forest Abkhazia", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player) + world.get_entrance("Forest Abkhazia").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - multiworld.get_entrance("The Maya", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player) + world.get_entrance("The Maya").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - multiworld.get_entrance("Land of Darkness", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player) + world.get_entrance("Land of Darkness").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - multiworld.get_entrance("The Fountain Room", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player) + world.get_entrance("The Fountain Room").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) # Win condition. - multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) + world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 78e56a794c85..290f4a60ac21 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -4,7 +4,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table -from .Options import rl_options +from .Options import RLOptions from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -33,20 +33,17 @@ class RLWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game = "Rogue Legacy" - option_definitions = rl_options + options_dataclass = RLOptions + options: RLOptions topology_present = True required_client_version = (0, 3, 5) web = RLWeb() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.code for name, data in location_table.items()} - - # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged. - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} + location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in rl_options} + return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): location_ids_used_per_game = { @@ -74,18 +71,18 @@ def generate_early(self): ) # Check validation of names. - additional_lady_names = len(self.get_setting("additional_lady_names").value) - additional_sir_names = len(self.get_setting("additional_sir_names").value) - if not self.get_setting("allow_default_names"): - if additional_lady_names < int(self.get_setting("number_of_children")): + additional_lady_names = len(self.options.additional_lady_names.value) + additional_sir_names = len(self.options.additional_sir_names.value) + if not self.options.allow_default_names: + if additional_lady_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - if additional_sir_names < int(self.get_setting("number_of_children")): + if additional_sir_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") def create_items(self): item_pool: List[RLItem] = [] @@ -95,110 +92,110 @@ def create_items(self): # Architect if name == "Architect": - if self.get_setting("architect") == "disabled": + if self.options.architect == "disabled": continue - if self.get_setting("architect") == "start_unlocked": + if self.options.architect == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("architect") == "early": + if self.options.architect == "early": self.multiworld.local_early_items[self.player]["Architect"] = 1 # Blacksmith and Enchantress if name == "Blacksmith" or name == "Enchantress": - if self.get_setting("vendors") == "start_unlocked": + if self.options.vendors == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("vendors") == "early": + if self.options.vendors == "early": self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 self.multiworld.local_early_items[self.player]["Enchantress"] = 1 # Haggling - if name == "Haggling" and self.get_setting("disable_charon"): + if name == "Haggling" and self.options.disable_charon: continue # Blueprints if data.category == "Blueprints": # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"): + if name == "Progressive Blueprints" and not self.options.progressive_blueprints: continue # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"): + elif name != "Progressive Blueprints" and self.options.progressive_blueprints: continue # Classes if data.category == "Classes": if name == "Progressive Knights": - if "Knight" not in self.get_setting("available_classes"): + if "Knight" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knight": + if self.options.starting_class == "knight": quantity = 1 if name == "Progressive Mages": - if "Mage" not in self.get_setting("available_classes"): + if "Mage" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "mage": + if self.options.starting_class == "mage": quantity = 1 if name == "Progressive Barbarians": - if "Barbarian" not in self.get_setting("available_classes"): + if "Barbarian" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "barbarian": + if self.options.starting_class == "barbarian": quantity = 1 if name == "Progressive Knaves": - if "Knave" not in self.get_setting("available_classes"): + if "Knave" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knave": + if self.options.starting_class == "knave": quantity = 1 if name == "Progressive Miners": - if "Miner" not in self.get_setting("available_classes"): + if "Miner" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "miner": + if self.options.starting_class == "miner": quantity = 1 if name == "Progressive Shinobis": - if "Shinobi" not in self.get_setting("available_classes"): + if "Shinobi" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "shinobi": + if self.options.starting_class == "shinobi": quantity = 1 if name == "Progressive Liches": - if "Lich" not in self.get_setting("available_classes"): + if "Lich" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "lich": + if self.options.starting_class == "lich": quantity = 1 if name == "Progressive Spellthieves": - if "Spellthief" not in self.get_setting("available_classes"): + if "Spellthief" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "spellthief": + if self.options.starting_class == "spellthief": quantity = 1 if name == "Dragons": - if "Dragon" not in self.get_setting("available_classes"): + if "Dragon" not in self.options.available_classes: continue if name == "Traitors": - if "Traitor" not in self.get_setting("available_classes"): + if "Traitor" not in self.options.available_classes: continue # Skills if name == "Health Up": - quantity = self.get_setting("health_pool") + quantity = self.options.health_pool.value elif name == "Mana Up": - quantity = self.get_setting("mana_pool") + quantity = self.options.mana_pool.value elif name == "Attack Up": - quantity = self.get_setting("attack_pool") + quantity = self.options.attack_pool.value elif name == "Magic Damage Up": - quantity = self.get_setting("magic_damage_pool") + quantity = self.options.magic_damage_pool.value elif name == "Armor Up": - quantity = self.get_setting("armor_pool") + quantity = self.options.armor_pool.value elif name == "Equip Up": - quantity = self.get_setting("equip_pool") + quantity = self.options.equip_pool.value elif name == "Crit Chance Up": - quantity = self.get_setting("crit_chance_pool") + quantity = self.options.crit_chance_pool.value elif name == "Crit Damage Up": - quantity = self.get_setting("crit_damage_pool") + quantity = self.options.crit_damage_pool.value # Ignore filler, it will be added in a later stage. if data.category == "Filler": @@ -215,7 +212,7 @@ def create_items(self): def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] - return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] + return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] def create_item(self, name: str) -> RLItem: data = item_table[name] @@ -226,10 +223,10 @@ def create_event(self, name: str) -> RLItem: return RLItem(name, data.classification, data.code, self.player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self, self.player) def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self) self._place_events() def _place_events(self): @@ -238,7 +235,7 @@ def _place_events(self): self.create_event("Defeat The Fountain")) # Khidr / Neo Khidr - if self.get_setting("khidr") == "vanilla": + if self.options.khidr == "vanilla": self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( self.create_event("Defeat Khidr")) else: @@ -246,7 +243,7 @@ def _place_events(self): self.create_event("Defeat Neo Khidr")) # Alexander / Alexander IV - if self.get_setting("alexander") == "vanilla": + if self.options.alexander == "vanilla": self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( self.create_event("Defeat Alexander")) else: @@ -254,7 +251,7 @@ def _place_events(self): self.create_event("Defeat Alexander IV")) # Ponce de Leon / Ponce de Freon - if self.get_setting("leon") == "vanilla": + if self.options.leon == "vanilla": self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( self.create_event("Defeat Ponce de Leon")) else: @@ -262,7 +259,7 @@ def _place_events(self): self.create_event("Defeat Ponce de Freon")) # Herodotus / Astrodotus - if self.get_setting("herodotus") == "vanilla": + if self.options.herodotus == "vanilla": self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( self.create_event("Defeat Herodotus")) else: diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py index 2639e618c678..3346476ba644 100644 --- a/worlds/rogue_legacy/test/__init__.py +++ b/worlds/rogue_legacy/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class RLTestBase(WorldTestBase):