From 2424b79626ee4e227ce3579c6c2d4a8cee7dca86 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 22 Nov 2024 08:33:27 -0700 Subject: [PATCH] OSRS: Fixes to Logic errors related to Max Skill Level determining when Regions are accessible (#4188) * Removes explicit indirect conditions * Changes special rules function add rule instead of setting, and call it unconditionally * Fixes issues in rule generation that have been around but unused the whole time * Finally moves rules out into a separate file. Fixes level-related logic * Removes redundant max skill level checks on canoes, since they're in the skill training rules now * For some reason, canoe logic assumed you could always walk from lumbridge to south varrock without farms. This has been fixed * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Quests now respect skill limits and can be excluded. Tasks that take multiple skills how actually check all skills * Adds alternative route for cooking that doesn't require fishing * Remove debug code --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/osrs/LogicCSV/locations_generated.py | 6 +- worlds/osrs/Names.py | 2 +- worlds/osrs/Rules.py | 337 +++++++++++++ worlds/osrs/__init__.py | 503 +++----------------- 4 files changed, 401 insertions(+), 447 deletions(-) create mode 100644 worlds/osrs/Rules.py diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py index 073e505a..2d617a70 100644 --- a/worlds/osrs/LogicCSV/locations_generated.py +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -57,11 +57,11 @@ location_rows = [ LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), - LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), - LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), - LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py index cc92439e..1a44aa38 100644 --- a/worlds/osrs/Names.py +++ b/worlds/osrs/Names.py @@ -31,7 +31,7 @@ class RegionNames(str, Enum): Mudskipper_Point = "Mudskipper Point" Karamja = "Karamja" Corsair_Cove = "Corsair Cove" - Wilderness = "The Wilderness" + Wilderness = "Wilderness" Crandor = "Crandor" # Resource Regions Egg = "Egg" diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py new file mode 100644 index 00000000..22a19934 --- /dev/null +++ b/worlds/osrs/Rules.py @@ -0,0 +1,337 @@ +""" + Ensures a target level can be reached with available resources + """ +from worlds.generic.Rules import CollectionRule, add_rule +from .Names import RegionNames, ItemNames + + +def get_fishing_skill_rule(level, player, options) -> CollectionRule: + if options.max_fishing_level < level: + return lambda state: False + + if options.brutal_grinds or level < 5: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) + if level < 20: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) + else: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) and \ + state.can_reach_region(RegionNames.Fly_Fish, player) + + +def get_mining_skill_rule(level, player, options) -> CollectionRule: + if options.max_mining_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \ + state.can_reach_region(RegionNames.Clay_Rock, player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or + state.can_reach_region(RegionNames.Clay_Rock, player)) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) + + +def get_woodcutting_skill_rule(level, player, options) -> CollectionRule: + if options.max_woodcutting_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) + else: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \ + state.can_reach_region(RegionNames.Willow_Tree, player) + + +def get_smithing_skill_rule(level, player, options) -> CollectionRule: + if options.max_smithing_level < level: + return lambda state: False + + if options.brutal_grinds: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + (state.can_reach_region(RegionNames.Anvil, player) or + state.can_reach_region(RegionNames.Lumbridge, player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + else: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Coal_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + + +def get_crafting_skill_rule(level, player, options): + if options.max_crafting_level < level: + return lambda state: False + + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach_region(RegionNames.Sheep, player) and \ + state.can_reach_region(RegionNames.Spinning_Wheel, player) + + def can_pot(state): + return state.can_reach_region(RegionNames.Clay_Rock, player) and \ + state.can_reach_region(RegionNames.Barbarian_Village, player) + + def can_tan(state): + return state.can_reach_region(RegionNames.Milk, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + def mould_access(state): + return state.can_reach_region(RegionNames.Al_Kharid, player) or \ + state.can_reach_region(RegionNames.Rimmington, player) + + def can_silver(state): + return state.can_reach_region(RegionNames.Silver_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + def can_gold(state): + return state.can_reach_region(RegionNames.Gold_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + if options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = get_smithing_skill_rule(40, player, options) + can_smelt_silver = get_smithing_skill_rule(20, player, options) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) + else: + return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ + (can_gold(state) and can_smelt_gold(state)) + + +def get_cooking_skill_rule(level, player, options) -> CollectionRule: + if options.max_cooking_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Milk, player) or \ + state.can_reach_region(RegionNames.Egg, player) or \ + state.can_reach_region(RegionNames.Shrimp, player) or \ + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + else: + can_catch_fly_fish = get_fishing_skill_rule(20, player, options) + + return lambda state: ( + (state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or + (state.can_reach_region(RegionNames.Port_Sarim, player)) + ) and ( + state.can_reach_region(RegionNames.Milk, player) or + state.can_reach_region(RegionNames.Egg, player) or + state.can_reach_region(RegionNames.Shrimp, player) or + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + ) + + +def get_runecraft_skill_rule(level, player, options) -> CollectionRule: + if options.max_runecraft_level < level: + return lambda state: False + if not options.brutal_grinds: + # Ensure access to the relevant altars + if level >= 5: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) + if level >= 9: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) + if level >= 14: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) + + +def get_magic_skill_rule(level, player, options) -> CollectionRule: + if options.max_magic_level < level: + return lambda state: False + + return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player) + + +def get_firemaking_skill_rule(level, player, options) -> CollectionRule: + if options.max_firemaking_level < level: + return lambda state: False + if not options.brutal_grinds: + if level >= 30: + can_chop_willows = get_woodcutting_skill_rule(30, player, options) + return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state) + if level >= 15: + can_chop_oaks = get_woodcutting_skill_rule(15, player, options) + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state) + # If brutal grinds are on, or if the level is less than 15, you can train it. + return lambda state: True + + +def get_skill_rule(skill, level, player, options) -> CollectionRule: + if skill.lower() == "fishing": + return get_fishing_skill_rule(level, player, options) + if skill.lower() == "mining": + return get_mining_skill_rule(level, player, options) + if skill.lower() == "woodcutting": + return get_woodcutting_skill_rule(level, player, options) + if skill.lower() == "smithing": + return get_smithing_skill_rule(level, player, options) + if skill.lower() == "crafting": + return get_crafting_skill_rule(level, player, options) + if skill.lower() == "cooking": + return get_cooking_skill_rule(level, player, options) + if skill.lower() == "runecraft": + return get_runecraft_skill_rule(level, player, options) + if skill.lower() == "magic": + return get_magic_skill_rule(level, player, options) + if skill.lower() == "firemaking": + return get_firemaking_skill_rule(level, player, options) + + return lambda state: True + + +def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options): + if outbound_region_name == RegionNames.Cooks_Guild: + add_rule(entrance, get_cooking_skill_rule(32, player, options)) + elif outbound_region_name == RegionNames.Crafting_Guild: + add_rule(entrance, get_crafting_skill_rule(40, player, options)) + elif outbound_region_name == RegionNames.Corsair_Cove: + # Need to be able to start Corsair Curse in addition to having the item + add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player)) + elif outbound_region_name == "Camdozaal*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player)) + elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player)) + + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options) + woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options) + woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options) + woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_all(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + + elif region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d3(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + elif region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d2(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + # Edgeville does not need to be checked, because it's already adjacent + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_d3) + elif region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d2) + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + elif region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_all(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_all) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + # Edgeville does not need to be checked, because it's already adjacent diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 58f23a2b..d6ddd638 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -1,12 +1,12 @@ import typing -from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState +from Fill import fill_restrictive, FillError from worlds.AutoWorld import WebWorld, World -from worlds.generic.Rules import add_rule, CollectionRule from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ chunksanity_special_region_names from .Locations import OSRSLocation, LocationRow - +from .Rules import * from .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames @@ -46,6 +46,7 @@ class OSRSWorld(World): web = OSRSWeb() base_id = 0x070000 data_version = 1 + explicit_indirect_conditions = False item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} @@ -61,6 +62,7 @@ class OSRSWorld(World): starting_area_item: str locations_by_category: typing.Dict[str, typing.List[LocationRow]] + available_QP_locations: typing.List[str] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -75,6 +77,7 @@ class OSRSWorld(World): self.starting_area_item = "" self.locations_by_category = {} + self.available_QP_locations = [] def generate_early(self) -> None: location_categories = [location_row.category for location_row in location_rows] @@ -90,9 +93,9 @@ class OSRSWorld(World): rnd = self.random starting_area = self.options.starting_area - + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT - if not hasattr(self.multiworld, "generation_is_fake"): + if not hasattr(self.multiworld, "generation_is_fake"): if starting_area.value == StartingArea.option_any_bank: self.starting_area_item = rnd.choice(starting_area_dict) elif starting_area.value < StartingArea.option_chunksanity: @@ -127,7 +130,6 @@ class OSRSWorld(World): starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) starting_entrance.connect(self.region_name_to_data[starting_area_region]) - def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -145,7 +147,8 @@ class OSRSWorld(World): # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + # if area hasn't been set, then we shouldn't connect it + if self.starting_area_item != "": if self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: @@ -164,11 +167,8 @@ class OSRSWorld(World): entrance.connect(self.region_name_to_data[parsed_outbound]) item_name = self.region_rows_by_name[parsed_outbound].itemReq - if "*" not in outbound_region_name and "*" not in item_name: - entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) - continue - - self.generate_special_rules_for(entrance, region_row, outbound_region_name) + entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player) + generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options) for resource_region in region_row.resources: if not resource_region: @@ -178,321 +178,34 @@ class OSRSWorld(World): if "*" not in resource_region: entrance.connect(self.region_name_to_data[resource_region]) else: - self.generate_special_rules_for(entrance, region_row, resource_region) entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options) self.roll_locations() - def generate_special_rules_for(self, entrance, region_row, outbound_region_name): - # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") - if outbound_region_name == RegionNames.Cooks_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - cooking_level_rule = self.get_skill_rule("cooking", 32) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - cooking_level_rule(state) - if self.options.brutal_grinds: - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - } - else: - # Level 15 cooking and higher requires level 20 fishing. - fishing_level_20_regions = { - RegionNames.Shrimp, - RegionNames.Port_Sarim, - } - cooking_level_32_regions = { - RegionNames.Milk, - RegionNames.Egg, - RegionNames.Shrimp, - RegionNames.Wheat, - RegionNames.Windmill, - RegionNames.Fly_Fish, - *fishing_level_20_regions, - } - for region_name in cooking_level_32_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Crafting_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - crafting_level_rule = self.get_skill_rule("crafting", 40) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - crafting_level_rule(state) - if self.options.brutal_grinds: - crafting_level_40_regions = { - # can_spin - RegionNames.Sheep, - RegionNames.Spinning_Wheel, - # can_pot - RegionNames.Clay_Rock, - RegionNames.Barbarian_Village, - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - } - else: - mould_access_regions = { - RegionNames.Al_Kharid, - RegionNames.Rimmington, - } - smithing_level_20_regions = { - RegionNames.Bronze_Ores, - RegionNames.Iron_Rock, - RegionNames.Furnace, - RegionNames.Anvil, - } - smithing_level_40_regions = { - *smithing_level_20_regions, - RegionNames.Coal_Rock, - } - crafting_level_40_regions = { - # can_tan - RegionNames.Milk, - RegionNames.Al_Kharid, - # can_silver - RegionNames.Silver_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_silver - *smithing_level_20_regions, - # can_gold - RegionNames.Gold_Rock, - RegionNames.Furnace, - *mould_access_regions, - # can_smelt_gold - *smithing_level_40_regions, - } - for region_name in crafting_level_40_regions: - self.multiworld.register_indirect_condition(self.get_region(region_name), entrance) - return - if outbound_region_name == RegionNames.Corsair_Cove: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - # Need to be able to start Corsair Curse in addition to having the item - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.can_reach(RegionNames.Falador_Farm, "Region", self.player) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) - - return - if outbound_region_name == "Camdozaal*": - item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.has(ItemNames.QP_Below_Ice_Mountain, self.player) - return - if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": - entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) - return - # Special logic for canoes - canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, - RegionNames.Edgeville, RegionNames.Wilderness] - if region_row.name in canoe_regions: - # Skill rules for greater distances - woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) - woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) - woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) - woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) - - def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int): - if self.options.brutal_grinds: - # No access to specific regions required. - return - # Currently, each level requirement requires everything from the previous level requirements, so the - # maximum level requirement can be taken. - max_level = max(levels, default=0) - max_level = min(max_level, self.options.max_woodcutting_level.value) - if 15 <= max_level < 30: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - elif 30 <= max_level: - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance) - self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance) - - if region_row.name == RegionNames.Lumbridge: - # Canoe Tree access for the Location - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # South of Varrock does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - - if region_row.name == RegionNames.South_Of_Varrock: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # Lumbridge does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Barbarian_Village: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - add_indirect_conditions_for_woodcutting_levels(entrance, 12) - # Edgeville does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if region_row.name == RegionNames.Edgeville: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Barbarian Village does not need to be checked, because it's already adjacent - # Wilderness does not need to be checked, because it's already adjacent - if region_row.name == RegionNames.Wilderness: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - add_indirect_conditions_for_woodcutting_levels(entrance, 57) - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - add_indirect_conditions_for_woodcutting_levels(entrance, 42) - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - add_indirect_conditions_for_woodcutting_levels(entrance, 27) - # Edgeville does not need to be checked, because it's already adjacent + def task_within_skill_levels(self, skills_required): + # Loop through each required skill. If any of its requirements are out of the defined limit, return false + for skill in skills_required: + max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level") + if skill.level > max_level_for_skill: + return False + return True def roll_locations(self): - locations_required = 0 generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + locations_required = 0 for item_row in item_rows: locations_required += item_row.amount locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 - # Quests are always added + # Quests are always added first, before anything else is rolled for i, location_row in enumerate(location_rows): if location_row.category in {"quest", "points", "goal"}: - self.create_and_add_location(i) - if location_row.category == "quest": - locations_added += 1 + if self.task_within_skill_levels(location_row.skills): + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 # Build up the weighted Task Pool rnd = self.random @@ -516,10 +229,9 @@ class OSRSWorld(World): task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] for task_type in task_types: - max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") tasks_for_this_type = [task for task in self.locations_by_category[task_type] - if task.skills[0].level <= max_level_for_task_type] + if self.task_within_skill_levels(task.skills)] if not self.options.progressive_tasks: rnd.shuffle(tasks_for_this_type) else: @@ -568,6 +280,7 @@ class OSRSWorld(World): self.add_location(task) locations_added += 1 + def add_location(self, location): index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] self.create_and_add_location(index) @@ -586,11 +299,15 @@ class OSRSWorld(World): def create_and_add_location(self, row_index) -> None: location_row = location_rows[row_index] - # print(f"Adding task {location_row.name}") + + # Quest Points are handled differently now, but in case this gets fed an older version of the data sheet, + # the points might still be listed in a different row + if location_row.category == "points": + return # Create Location location_id = self.base_id + row_index - if location_row.category == "points" or location_row.category == "goal": + if location_row.category == "goal": location_id = None location = OSRSLocation(self.player, location_row.name, location_id) self.location_name_to_data[location_row.name] = location @@ -602,6 +319,14 @@ class OSRSWorld(World): location.parent_region = region region.locations.append(location) + # If it's a quest, generate a "Points" location we'll add an event to + if location_row.category == "quest": + points_name = location_row.name.replace("Quest:", "Points:") + points_location = OSRSLocation(self.player, points_name) + self.location_name_to_data[points_name] = points_location + points_location.parent_region = region + region.locations.append(points_location) + def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. @@ -612,18 +337,26 @@ class OSRSWorld(World): "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Below_Ice_Mountain"] - for qp_attr_name in quest_attr_names: - loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") - item_name = getattr(ItemNames, f"QP_{qp_attr_name}") - self.multiworld.get_location(loc_name, self.player) \ - .place_locked_item(self.create_event(item_name)) for quest_attr_name in quest_attr_names: qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + qp_loc = self.location_name_to_data.get(qp_loc_name) + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") - add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( - self.multiworld.get_location(q_loc_name, self.player).can_reach(state) - )) + q_loc = self.location_name_to_data.get(q_loc_name) + + # Checks to make sure the task is actually in the list before trying to create its rules + if qp_loc and q_loc: + # Create the QP Event Item + item_name = getattr(ItemNames, f"QP_{quest_attr_name}") + qp_loc.place_locked_item(self.create_event(item_name)) + + # If a quest is excluded, don't actually consider it for quest point progression + if q_loc_name not in self.options.exclude_locations: + self.available_QP_locations.append(item_name) + + # Set the access rule for the QP Location + add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) # place "Victory" at "Dragon Slayer" and set collection as win condition self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ @@ -639,7 +372,7 @@ class OSRSWorld(World): lambda state, region_required=region_required: state.can_reach(region_required, "Region", self.player)) for skill_req in location_row.skills: - add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options)) for item_req in location_row.items: add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) if location_row.qp: @@ -664,124 +397,8 @@ class OSRSWorld(World): def quest_points(self, state): qp = 0 - for qp_event in QP_Items: + for qp_event in self.available_QP_locations: if state.has(qp_event, self.player): qp += int(qp_event[0]) return qp - """ - Ensures a target level can be reached with available resources - """ - - def get_skill_rule(self, skill, level) -> CollectionRule: - if skill.lower() == "fishing": - if self.options.brutal_grinds or level < 5: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) - if level < 20: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ - state.can_reach(RegionNames.Fly_Fish, "Region", self.player) - if skill.lower() == "mining": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ - state.can_reach(RegionNames.Clay_Rock, "Region", self.player) - else: - # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for - return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or - state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) - if skill.lower() == "woodcutting": - if self.options.brutal_grinds or level < 15: - # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. - # Even the desert. - return lambda state: True - if level < 30: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ - state.can_reach(RegionNames.Willow_Tree, "Region", self.player) - if skill.lower() == "smithing": - if self.options.brutal_grinds: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) - if level < 15: - # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included - # in the "Anvil" resource region. We still need to check for it though. - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - (state.can_reach(RegionNames.Anvil, "Region", self.player) or - state.can_reach(RegionNames.Lumbridge, "Region", self.player)) - if level < 30: - # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - if skill.lower() == "crafting": - # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable - def can_spin(state): - return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ - state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) - - def can_pot(state): - return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) - - def can_tan(state): - return state.can_reach(RegionNames.Milk, "Region", self.player) and \ - state.can_reach(RegionNames.Al_Kharid, "Region", self.player) - - def mould_access(state): - return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ - state.can_reach(RegionNames.Rimmington, "Region", self.player) - - def can_silver(state): - - return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - def can_gold(state): - return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - if self.options.brutal_grinds or level < 5: - return lambda state: can_spin(state) or can_pot(state) or can_tan(state) - - can_smelt_gold = self.get_skill_rule("smithing", 40) - can_smelt_silver = self.get_skill_rule("smithing", 20) - if level < 16: - return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) - else: - return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ - (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "cooking": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ - state.can_reach(RegionNames.Egg, "Region", self.player) or \ - state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player)) - else: - can_catch_fly_fish = self.get_skill_rule("fishing", 20) - return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ - can_catch_fly_fish(state) and \ - (state.can_reach(RegionNames.Milk, "Region", self.player) or - state.can_reach(RegionNames.Egg, "Region", self.player) or - state.can_reach(RegionNames.Shrimp, "Region", self.player) or - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player))) - if skill.lower() == "runecraft": - return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) - if skill.lower() == "magic": - return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) - - return lambda state: True