import typing from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld 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 .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames from .LogicCSV.LogicCSVToPython import data_csv_tag from .LogicCSV.items_generated import item_rows from .LogicCSV.locations_generated import location_rows from .LogicCSV.regions_generated import region_rows from .LogicCSV.resources_generated import resource_rows from .Regions import RegionRow, ResourceRow class OSRSWeb(WebWorld): theme = "stone" setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld", "English", "docs/setup_en.md", "setup/en", ["digiholic"] ) tutorials = [setup_en] class OSRSWorld(World): """ The best retro fantasy MMORPG on the planet. Old School is RuneScape but… older! This is the open world you know and love, but as it was in 2007. The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild! """ game = "Old School Runescape" options_dataclass = OSRSOptions options: OSRSOptions topology_present = True web = OSRSWeb() base_id = 0x070000 data_version = 1 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))} region_name_to_data: typing.Dict[str, Region] location_name_to_data: typing.Dict[str, OSRSLocation] location_rows_by_name: typing.Dict[str, LocationRow] region_rows_by_name: typing.Dict[str, RegionRow] resource_rows_by_name: typing.Dict[str, ResourceRow] item_rows_by_name: typing.Dict[str, ItemRow] starting_area_item: str locations_by_category: typing.Dict[str, typing.List[LocationRow]] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) self.region_name_to_data = {} self.location_name_to_data = {} self.location_rows_by_name = {} self.region_rows_by_name = {} self.resource_rows_by_name = {} self.item_rows_by_name = {} self.starting_area_item = "" self.locations_by_category = {} def generate_early(self) -> None: location_categories = [location_row.category for location_row in location_rows] self.locations_by_category = {category: [location_row for location_row in location_rows if location_row.category == category] for category in location_categories} self.location_rows_by_name = {loc_row.name: loc_row for loc_row in location_rows} self.region_rows_by_name = {reg_row.name: reg_row for reg_row in region_rows} self.resource_rows_by_name = {rec_row.name: rec_row for rec_row in resource_rows} self.item_rows_by_name = {it_row.name: it_row for it_row in item_rows} 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 starting_area.value == StartingArea.option_any_bank: self.starting_area_item = rnd.choice(starting_area_dict) elif starting_area.value < StartingArea.option_chunksanity: self.starting_area_item = starting_area_dict[starting_area.value] else: self.starting_area_item = rnd.choice(chunksanity_starting_chunks) # Set Starting Chunk self.multiworld.push_precollected(self.create_item(self.starting_area_item)) """ This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. _Make sure to update that value whenever the CSVs change!_ """ def fill_slot_data(self): data = self.options.as_dict("brutal_grinds") data["data_csv_tag"] = data_csv_tag data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv return data def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: if "starting_area" in slot_data: self.starting_area_item = slot_data["starting_area"] menu_region = self.multiworld.get_region("Menu",self.player) menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot if self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: starting_area_region = self.starting_area_item[6:] # len("Area: ") starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") 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 during generate_early or basic as well. """ # First, create the "Menu" region to start menu_region = self.create_region("Menu") for region_row in region_rows: self.create_region(region_row.name) for resource_row in resource_rows: self.create_region(resource_row.name) # 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 self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: starting_area_region = self.starting_area_item[6:] # len("Area: ") starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") 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]) # Create entrances between regions for region_row in region_rows: region = self.region_name_to_data[region_row.name] for outbound_region_name in region_row.connections: parsed_outbound = outbound_region_name.replace('*', '') entrance = region.create_exit(f"{region_row.name}->{parsed_outbound}") 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) for resource_region in region_row.resources: if not resource_region: continue entrance = region.create_exit(f"{region_row.name}->{resource_region.replace('*', '')}") 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('*', '')]) 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) 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) 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) 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) 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 if outbound_region_name == RegionNames.Edgeville: entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ and self.options.max_woodcutting_level >= 42 if outbound_region_name == RegionNames.Wilderness: entrance.access_rule = lambda state: woodcutting_rule_all(state) \ and self.options.max_woodcutting_level >= 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) 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 if outbound_region_name == RegionNames.Edgeville: entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ and self.options.max_woodcutting_level >= 27 if outbound_region_name == RegionNames.Wilderness: entrance.access_rule = lambda state: woodcutting_rule_all(state) \ and self.options.max_woodcutting_level >= 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) 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 if outbound_region_name == RegionNames.South_Of_Varrock: entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ and self.options.max_woodcutting_level >= 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 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) 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 if outbound_region_name == RegionNames.South_Of_Varrock: entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ and self.options.max_woodcutting_level >= 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) 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 if outbound_region_name == RegionNames.South_Of_Varrock: entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ and self.options.max_woodcutting_level >= 42 if outbound_region_name == RegionNames.Barbarian_Village: entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ and self.options.max_woodcutting_level >= 27 # Edgeville does not need to be checked, because it's already adjacent def roll_locations(self): locations_required = 0 generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override 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 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 # Build up the weighted Task Pool rnd = self.random # Start with the minimum general tasks general_tasks = [task for task in self.locations_by_category["general"]] if not self.options.progressive_tasks: rnd.shuffle(general_tasks) else: general_tasks.reverse() for i in range(self.options.minimum_general_tasks): task = general_tasks.pop() self.add_location(task) locations_added += 1 general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {} weights_per_task_type: typing.Dict[str, int] = {} 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 not self.options.progressive_tasks: rnd.shuffle(tasks_for_this_type) else: tasks_for_this_type.reverse() tasks_for_this_type = tasks_for_this_type[:max_amount_for_task_type] weight_for_this_type = getattr(self.options, f"{task_type}_task_weight") if weight_for_this_type > 0 and tasks_for_this_type: tasks_per_task_type[task_type] = tasks_for_this_type weights_per_task_type[task_type] = weight_for_this_type # Build a list of collections and weights in a matching order for rnd.choices later all_tasks = [] all_weights = [] for task_type in task_types: if task_type in tasks_per_task_type: all_tasks.append(tasks_per_task_type[task_type]) all_weights.append(weights_per_task_type[task_type]) # Even after the initial forced generals, they can still be rolled randomly if general_weight > 0: all_tasks.append(general_tasks) all_weights.append(general_weight) while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0): if all_tasks: chosen_task = rnd.choices(all_tasks, all_weights)[0] if chosen_task: task = chosen_task.pop() self.add_location(task) locations_added += 1 # This isn't an else because chosen_task can become empty in the process of resolving the above block # We still want to clear this list out while we're doing that if not chosen_task: index = all_tasks.index(chosen_task) del all_tasks[index] del all_weights[index] else: if len(general_tasks) == 0: raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.") task = general_tasks.pop() 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) def create_items(self) -> None: for item_row in item_rows: if item_row.name != self.starting_area_item: for c in range(item_row.amount): item = self.create_item(item_row.name) self.multiworld.itempool.append(item) def get_filler_item_name(self) -> str: return self.random.choice( [ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon]) def create_and_add_location(self, row_index) -> None: location_row = location_rows[row_index] # print(f"Adding task {location_row.name}") # Create Location location_id = self.base_id + row_index if location_row.category == "points" or 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 # Add the location to its first region, or if it doesn't belong to one, to Menu region = self.region_name_to_data["Menu"] if location_row.regions: region = self.region_name_to_data[location_row.regions[0]] location.parent_region = region region.locations.append(location) def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. """ quest_attr_names = ["Cooks_Assistant", "Demon_Slayer", "Restless_Ghost", "Romeo_Juliet", "Sheep_Shearer", "Shield_of_Arrav", "Ernest_the_Chicken", "Vampyre_Slayer", "Imp_Catcher", "Prince_Ali_Rescue", "Dorics_Quest", "Black_Knights_Fortress", "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}") 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) )) # place "Victory" at "Dragon Slayer" and set collection as win condition self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ .place_locked_item(self.create_event("Victory")) self.multiworld.completion_condition[self.player] = lambda state: (state.has("Victory", self.player)) for location_name, location in self.location_name_to_data.items(): location_row = self.location_rows_by_name[location_name] # Set up requirements for region for region_required_name in location_row.regions: region_required = self.region_name_to_data[region_required_name] add_rule(location, 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)) 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: add_rule(location, lambda state, location_row=location_row: self.quest_points(state) > location_row.qp) def create_region(self, name: str) -> "Region": region = Region(name, self.player, self.multiworld) self.region_name_to_data[name] = region self.multiworld.regions.append(region) return region def create_item(self, item_name: str) -> "Item": items = [item for item in item_rows if item.name == item_name] assert len(items) > 0, f"No matching item found for name {item_name} for player {self.player_name}" item = items[0] index = item_rows.index(item) return OSRSItem(item.name, item.progression, self.base_id + index, self.player) def create_event(self, event: str): # while we are at it, we can also add a helper to create events return OSRSItem(event, ItemClassification.progression, None, self.player) def quest_points(self, state): qp = 0 for qp_event in QP_Items: 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