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>
This commit is contained in:
digiholic 2024-11-22 08:33:27 -07:00 committed by GitHub
parent d4b1351c99
commit 2424b79626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 401 additions and 447 deletions

View File

@ -57,11 +57,11 @@ location_rows = [
LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), 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('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('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 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('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 Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 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('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 an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),

View File

@ -31,7 +31,7 @@ class RegionNames(str, Enum):
Mudskipper_Point = "Mudskipper Point" Mudskipper_Point = "Mudskipper Point"
Karamja = "Karamja" Karamja = "Karamja"
Corsair_Cove = "Corsair Cove" Corsair_Cove = "Corsair Cove"
Wilderness = "The Wilderness" Wilderness = "Wilderness"
Crandor = "Crandor" Crandor = "Crandor"
# Resource Regions # Resource Regions
Egg = "Egg" Egg = "Egg"

337
worlds/osrs/Rules.py Normal file
View File

@ -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

View File

@ -1,12 +1,12 @@
import typing 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.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, \ from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow from .Locations import OSRSLocation, LocationRow
from .Rules import *
from .Options import OSRSOptions, StartingArea from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames from .Names import LocationNames, ItemNames, RegionNames
@ -46,6 +46,7 @@ class OSRSWorld(World):
web = OSRSWeb() web = OSRSWeb()
base_id = 0x070000 base_id = 0x070000
data_version = 1 data_version = 1
explicit_indirect_conditions = False
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} 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))} 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 starting_area_item: str
locations_by_category: typing.Dict[str, typing.List[LocationRow]] locations_by_category: typing.Dict[str, typing.List[LocationRow]]
available_QP_locations: typing.List[str]
def __init__(self, multiworld: MultiWorld, player: int): def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player) super().__init__(multiworld, player)
@ -75,6 +77,7 @@ class OSRSWorld(World):
self.starting_area_item = "" self.starting_area_item = ""
self.locations_by_category = {} self.locations_by_category = {}
self.available_QP_locations = []
def generate_early(self) -> None: def generate_early(self) -> None:
location_categories = [location_row.category for location_row in location_rows] location_categories = [location_row.category for location_row in location_rows]
@ -127,7 +130,6 @@ class OSRSWorld(World):
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) 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]) starting_entrance.connect(self.region_name_to_data[starting_area_region])
def create_regions(self) -> None: 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 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. # 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 # 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: if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item] starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else: else:
@ -164,11 +167,8 @@ class OSRSWorld(World):
entrance.connect(self.region_name_to_data[parsed_outbound]) entrance.connect(self.region_name_to_data[parsed_outbound])
item_name = self.region_rows_by_name[parsed_outbound].itemReq 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.replace("*",""): state.has(item_name, self.player)
entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
continue
self.generate_special_rules_for(entrance, region_row, outbound_region_name)
for resource_region in region_row.resources: for resource_region in region_row.resources:
if not resource_region: if not resource_region:
@ -178,318 +178,31 @@ class OSRSWorld(World):
if "*" not in resource_region: if "*" not in resource_region:
entrance.connect(self.region_name_to_data[resource_region]) entrance.connect(self.region_name_to_data[resource_region])
else: else:
self.generate_special_rules_for(entrance, region_row, resource_region)
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) 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() self.roll_locations()
def generate_special_rules_for(self, entrance, region_row, outbound_region_name): def task_within_skill_levels(self, skills_required):
# print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") # Loop through each required skill. If any of its requirements are out of the defined limit, return false
if outbound_region_name == RegionNames.Cooks_Guild: for skill in skills_required:
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level")
cooking_level_rule = self.get_skill_rule("cooking", 32) if skill.level > max_level_for_skill:
entrance.access_rule = lambda state: state.has(item_name, self.player) and \ return False
cooking_level_rule(state) return True
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 roll_locations(self): def roll_locations(self):
locations_required = 0
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
locations_required = 0
for item_row in item_rows: for item_row in item_rows:
locations_required += item_row.amount 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 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): for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}: if location_row.category in {"quest", "points", "goal"}:
if self.task_within_skill_levels(location_row.skills):
self.create_and_add_location(i) self.create_and_add_location(i)
if location_row.category == "quest": if location_row.category == "quest":
locations_added += 1 locations_added += 1
@ -516,10 +229,9 @@ class OSRSWorld(World):
task_types = ["prayer", "magic", "runecraft", "mining", "crafting", task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types: 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") 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] 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: if not self.options.progressive_tasks:
rnd.shuffle(tasks_for_this_type) rnd.shuffle(tasks_for_this_type)
else: else:
@ -568,6 +280,7 @@ class OSRSWorld(World):
self.add_location(task) self.add_location(task)
locations_added += 1 locations_added += 1
def add_location(self, location): def add_location(self, location):
index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0]
self.create_and_add_location(index) self.create_and_add_location(index)
@ -586,11 +299,15 @@ class OSRSWorld(World):
def create_and_add_location(self, row_index) -> None: def create_and_add_location(self, row_index) -> None:
location_row = location_rows[row_index] 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 # Create Location
location_id = self.base_id + row_index 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_id = None
location = OSRSLocation(self.player, location_row.name, location_id) location = OSRSLocation(self.player, location_row.name, location_id)
self.location_name_to_data[location_row.name] = location self.location_name_to_data[location_row.name] = location
@ -602,6 +319,14 @@ class OSRSWorld(World):
location.parent_region = region location.parent_region = region
region.locations.append(location) 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: def set_rules(self) -> None:
""" """
called to set access and item rules on locations and entrances. 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", "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure",
"Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot",
"Below_Ice_Mountain"] "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: for quest_attr_name in quest_attr_names:
qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") 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}") 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: ( q_loc = self.location_name_to_data.get(q_loc_name)
self.multiworld.get_location(q_loc_name, self.player).can_reach(state)
)) # 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 # place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ 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", lambda state, region_required=region_required: state.can_reach(region_required, "Region",
self.player)) self.player))
for skill_req in location_row.skills: 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: for item_req in location_row.items:
add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player))
if location_row.qp: if location_row.qp:
@ -664,124 +397,8 @@ class OSRSWorld(World):
def quest_points(self, state): def quest_points(self, state):
qp = 0 qp = 0
for qp_event in QP_Items: for qp_event in self.available_QP_locations:
if state.has(qp_event, self.player): if state.has(qp_event, self.player):
qp += int(qp_event[0]) qp += int(qp_event[0])
return qp 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