mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00

All entrances to Cooks_Guild and Crafting_Guild and all entrances using special logic for canoes were missing indirect conditions for the regions that the cooking, crafting and woodcutting skill rules require access to.
788 lines
46 KiB
Python
788 lines
46 KiB
Python
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)
|
|
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):
|
|
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
|