OSRS: Implement New Game (#1976)

* MMBN3: Press program now has proper color index when received remotely

* Initial commit of OSRS untangled from MMBN3 branch

* Fixes some broken region connections

* Removes some locations

* Rearranges locations to fill in slots left by removed locations

* Adds starting area rando

* Moves Oak and Willow trees to resource regions

* Fixes various PEP8 violations

* Refactor of regions

* Fixes variable capture issue with region rules

* Partial completion of brutal grind logic

* Finishes can_reach_skill function

* Adds skill requirements to location rules, fixes regions rules

* Adds documentation for OSRS

* Removes match statement

* Updates Data Version to test mode to prevent item name caching

* Fixes starting spawn logic for east varrock

* Fixes river lum crossing logic to not assume you can phase across water

* Prevents equipping items when you haven't unlocked them

* Changes canoe logic to not require huge levels

* Skeletoning out some data I'll need for variable task system

* Adds csvs and parser for logic

* Adds Items parsing

* Fixes the spawning logic to not default to Chunksanity when you didn't pick it

* Begins adding generation rules for data-driven logic

* Moves region handling and location creating to different methods

* Adds logic limits to Options

* Begun the location generation has

* Randomly generates tasks for each skill until populated

* Mopping up improper names, adding custom logic, and fixes location rolling

* Drastically cleans up the location rolling loop

* Modifies generation to properly use local variables and pass unit tests

* Game is now generating, but rules don't seem to work

* Lambda capture, my old nemesis. We meet again

* Fixes issue with Corsair Cove item requirement causing logic loop

* Okay one more fix, another variable capture

* On second thought lets not have skull sceptre tasks. 'Tis a silly place

* Removes QP from item pool (they're events not items)

* Removes Stronghold floor tasks, no varbit to track them

* Loads CSV with pkutil so it can be used in apworld

* Fixes logic of skill tasks and adds QP requirements to long grinds

* Fixes pathing in pkgutil call

* Better handling for empty task categories, no longer throws errors

* Fixes order for progressive tasks, removes un-checkable spider task

* Fixes logic issues related to stew and the Blurite caves

* Fixes issues generating causing tests to sporadically fail

* Adds missing task that caused off-by-one error

* Updates to new Options API

* Updates generation to function properly with the Universal Tracker (Thanks Faris)

* Replaces runtime CSV parsing with pre-made python files generated from CSVs

* Switches to self.random and uses random.choice instead of doing it manually

* Fixes to typing, variable names, iterators, and continue conditions

* Replaces Name classes with Enums

* Fixes parse error on region special rules

* Skill requirements check now returns an accessrule instead of being one that checks options

* Updates documentation and setup guide

* Adjusts maximum numbers for combat and general tasks

* Fixes region names so dictionary lookup works for chunksanity

* Update worlds/osrs/docs/en_Old School Runescape.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Update worlds/osrs/docs/en_Old School Runescape.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* Updates readme.md and codeowners doc

* Removes erroneous East Varrock -> Al Kharid connection

* Changes to canoe logic to account for woodcutting level options

* Fixes embarassing typo on 'Edgeville'

* Moves Logic CSVs to separate repository, addresses suggested changes on PR

* Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main

* Removes task types with weight 0 from the list of rollable tasks

* Missed another place that the task type had to be removed if 0 weight

* Prevents adding an empty task weight if levels are too restrictive for tasks to be added

* Removes giant blank space in error message

* Adds player name to error for not having enough available tasks

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
This commit is contained in:
digiholic 2024-08-06 15:13:11 -06:00 committed by GitHub
parent 90446ad175
commit 8ddb49f071
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2052 additions and 0 deletions

View File

@ -72,6 +72,7 @@ Currently, the following games are supported:
* Aquaria
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time
* Old School Runescape
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -115,6 +115,9 @@
# Ocarina of Time
/worlds/oot/ @espeon65536
# Old School Runescape
/worlds/osrs @digiholic
# Overcooked! 2
/worlds/overcooked2/ @toasterparty

85
worlds/osrs/Items.py Normal file
View File

@ -0,0 +1,85 @@
import typing
from BaseClasses import Item, ItemClassification
from .Names import ItemNames
class ItemRow(typing.NamedTuple):
name: str
amount: int
progression: ItemClassification
class OSRSItem(Item):
game: str = "Old School Runescape"
QP_Items: typing.List[str] = [
ItemNames.QP_Cooks_Assistant,
ItemNames.QP_Demon_Slayer,
ItemNames.QP_Restless_Ghost,
ItemNames.QP_Romeo_Juliet,
ItemNames.QP_Sheep_Shearer,
ItemNames.QP_Shield_of_Arrav,
ItemNames.QP_Ernest_the_Chicken,
ItemNames.QP_Vampyre_Slayer,
ItemNames.QP_Imp_Catcher,
ItemNames.QP_Prince_Ali_Rescue,
ItemNames.QP_Dorics_Quest,
ItemNames.QP_Black_Knights_Fortress,
ItemNames.QP_Witchs_Potion,
ItemNames.QP_Knights_Sword,
ItemNames.QP_Goblin_Diplomacy,
ItemNames.QP_Pirates_Treasure,
ItemNames.QP_Rune_Mysteries,
ItemNames.QP_Misthalin_Mystery,
ItemNames.QP_Corsair_Curse,
ItemNames.QP_X_Marks_the_Spot,
ItemNames.QP_Below_Ice_Mountain
]
starting_area_dict: typing.Dict[int, str] = {
0: ItemNames.Lumbridge,
1: ItemNames.Al_Kharid,
2: ItemNames.Central_Varrock,
3: ItemNames.West_Varrock,
4: ItemNames.Edgeville,
5: ItemNames.Falador,
6: ItemNames.Draynor_Village,
7: ItemNames.Wilderness,
}
chunksanity_starting_chunks: typing.List[str] = [
ItemNames.Lumbridge,
ItemNames.Lumbridge_Swamp,
ItemNames.Lumbridge_Farms,
ItemNames.HAM_Hideout,
ItemNames.Draynor_Village,
ItemNames.Draynor_Manor,
ItemNames.Wizards_Tower,
ItemNames.Al_Kharid,
ItemNames.Citharede_Abbey,
ItemNames.South_Of_Varrock,
ItemNames.Central_Varrock,
ItemNames.Varrock_Palace,
ItemNames.East_Of_Varrock,
ItemNames.West_Varrock,
ItemNames.Edgeville,
ItemNames.Barbarian_Village,
ItemNames.Monastery,
ItemNames.Ice_Mountain,
ItemNames.Dwarven_Mines,
ItemNames.Falador,
ItemNames.Falador_Farm,
ItemNames.Crafting_Guild,
ItemNames.Rimmington,
ItemNames.Port_Sarim,
ItemNames.Mudskipper_Point,
ItemNames.Wilderness
]
# Some starting areas contain multiple regions, so if that area is rolled for Chunksanity, we need to map it to one
chunksanity_special_region_names: typing.Dict[str, str] = {
ItemNames.Lumbridge_Farms: 'Lumbridge Farms East',
ItemNames.Crafting_Guild: 'Crafting Guild Outskirts',
}

21
worlds/osrs/Locations.py Normal file
View File

@ -0,0 +1,21 @@
import typing
from BaseClasses import Location
class SkillRequirement(typing.NamedTuple):
skill: str
level: int
class LocationRow(typing.NamedTuple):
name: str
category: str
regions: typing.List[str]
skills: typing.List[SkillRequirement]
items: typing.List[str]
qp: int
class OSRSLocation(Location):
game: str = "Old School Runescape"

View File

@ -0,0 +1,144 @@
"""
This is a utility file that converts logic in the form of CSV files into Python files that can be imported and used
directly by the world implementation. Whenever the logic files are updated, this script should be run to re-generate
the python files containing the data.
"""
import requests
# The CSVs are updated at this repository to be shared between generator and client.
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
# The Github tag of the CSVs this was generated with
data_csv_tag = "v1.5"
if __name__ == "__main__":
import sys
import os
import csv
import typing
# makes this module runnable from its world folder. Shamelessly stolen from Subnautica
sys.path.remove(os.path.dirname(__file__))
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
os.chdir(new_home)
sys.path.append(new_home)
def load_location_csv():
this_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile:
locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n")
locPyFile.write("\n")
locPyFile.write("location_rows = [\n")
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
locations_reader = csv.reader(req.text.splitlines())
for row in locations_reader:
row_line = "LocationRow("
row_line += str_format(row[0])
row_line += str_format(row[1].lower())
region_strings = row[2].split(", ") if row[2] else []
row_line += f"{str_list_to_py(region_strings)}, "
skill_strings = row[3].split(", ")
row_line += "["
if skill_strings:
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
if split_skills:
for split in split_skills:
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
row_line += "], "
item_strings = row[4].split(", ") if row[4] else []
row_line += f"{str_list_to_py(item_strings)}, "
row_line += f"{row[5]})" if row[5] != "" else "0)"
locPyFile.write(f"\t{row_line},\n")
locPyFile.write("]\n")
def load_region_csv():
this_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile:
regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
regPyFile.write("from ..Regions import RegionRow\n")
regPyFile.write("\n")
regPyFile.write("region_rows = [\n")
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
regions_reader = csv.reader(req.text.splitlines())
for row in regions_reader:
row_line = "RegionRow("
row_line += str_format(row[0])
row_line += str_format(row[1])
connections = row[2].replace("'", "\\'")
row_line += f"{str_list_to_py(connections.split(', '))}, "
resources = row[3].replace("'", "\\'")
row_line += f"{str_list_to_py(resources.split(', '))})"
regPyFile.write(f"\t{row_line},\n")
regPyFile.write("]\n")
def load_resource_csv():
this_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile:
resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
resPyFile.write("from ..Regions import ResourceRow\n")
resPyFile.write("\n")
resPyFile.write("resource_rows = [\n")
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
resource_reader = csv.reader(req.text.splitlines())
for row in resource_reader:
name = row[0].replace("'", "\\'")
row_line = f"ResourceRow('{name}')"
resPyFile.write(f"\t{row_line},\n")
resPyFile.write("]\n")
def load_item_csv():
this_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile:
itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
itemPyfile.write("from BaseClasses import ItemClassification\n")
itemPyfile.write("from ..Items import ItemRow\n")
itemPyfile.write("\n")
itemPyfile.write("item_rows = [\n")
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
item_reader = csv.reader(req.text.splitlines())
for row in item_reader:
row_line = "ItemRow("
row_line += str_format(row[0])
row_line += f"{row[1]}, "
row_line += f"ItemClassification.{row[2]})"
itemPyfile.write(f"\t{row_line},\n")
itemPyfile.write("]\n")
def str_format(s) -> str:
ret_str = s.replace("'", "\\'")
return f"'{ret_str}', "
def str_list_to_py(str_list) -> str:
ret_str = "["
for s in str_list:
ret_str += f"'{s}', "
ret_str += "]"
return ret_str
load_location_csv()
print("Generated locations py")
load_region_csv()
print("Generated regions py")
load_resource_csv()
print("Generated resource py")
load_item_csv()
print("Generated item py")

View File

@ -0,0 +1,43 @@
"""
This file was auto generated by LogicCSVToPython.py
"""
from BaseClasses import ItemClassification
from ..Items import ItemRow
item_rows = [
ItemRow('Area: Lumbridge', 1, ItemClassification.progression),
ItemRow('Area: Lumbridge Swamp', 1, ItemClassification.progression),
ItemRow('Area: HAM Hideout', 1, ItemClassification.progression),
ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression),
ItemRow('Area: South of Varrock', 1, ItemClassification.progression),
ItemRow('Area: East Varrock', 1, ItemClassification.progression),
ItemRow('Area: Central Varrock', 1, ItemClassification.progression),
ItemRow('Area: Varrock Palace', 1, ItemClassification.progression),
ItemRow('Area: West Varrock', 1, ItemClassification.progression),
ItemRow('Area: Edgeville', 1, ItemClassification.progression),
ItemRow('Area: Barbarian Village', 1, ItemClassification.progression),
ItemRow('Area: Draynor Manor', 1, ItemClassification.progression),
ItemRow('Area: Falador', 1, ItemClassification.progression),
ItemRow('Area: Dwarven Mines', 1, ItemClassification.progression),
ItemRow('Area: Ice Mountain', 1, ItemClassification.progression),
ItemRow('Area: Monastery', 1, ItemClassification.progression),
ItemRow('Area: Falador Farms', 1, ItemClassification.progression),
ItemRow('Area: Port Sarim', 1, ItemClassification.progression),
ItemRow('Area: Mudskipper Point', 1, ItemClassification.progression),
ItemRow('Area: Karamja', 1, ItemClassification.progression),
ItemRow('Area: Crandor', 1, ItemClassification.progression),
ItemRow('Area: Rimmington', 1, ItemClassification.progression),
ItemRow('Area: Crafting Guild', 1, ItemClassification.progression),
ItemRow('Area: Draynor Village', 1, ItemClassification.progression),
ItemRow('Area: Wizard Tower', 1, ItemClassification.progression),
ItemRow('Area: Corsair Cove', 1, ItemClassification.progression),
ItemRow('Area: Al Kharid', 1, ItemClassification.progression),
ItemRow('Area: Citharede Abbey', 1, ItemClassification.progression),
ItemRow('Area: Wilderness', 1, ItemClassification.progression),
ItemRow('Progressive Armor', 6, ItemClassification.progression),
ItemRow('Progressive Weapons', 6, ItemClassification.progression),
ItemRow('Progressive Tools', 6, ItemClassification.useful),
ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful),
ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful),
ItemRow('Progressive Magic', 2, ItemClassification.useful),
]

View File

@ -0,0 +1,127 @@
"""
This file was auto generated by LogicCSVToPython.py
"""
from ..Locations import LocationRow, SkillRequirement
location_rows = [
LocationRow('Quest: Cook\'s Assistant', 'quest', ['Lumbridge', 'Wheat', 'Windmill', 'Egg', 'Milk', ], [], [], 0),
LocationRow('Quest: Demon Slayer', 'quest', ['Central Varrock', 'Varrock Palace', 'Wizard Tower', 'South of Varrock', ], [], [], 0),
LocationRow('Quest: The Restless Ghost', 'quest', ['Lumbridge', 'Lumbridge Swamp', 'Wizard Tower', ], [], [], 0),
LocationRow('Quest: Romeo & Juliet', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0),
LocationRow('Quest: Sheep Shearer', 'quest', ['Lumbridge Farms West', 'Spinning Wheel', ], [], [], 0),
LocationRow('Quest: Shield of Arrav', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0),
LocationRow('Quest: Ernest the Chicken', 'quest', ['Draynor Manor', ], [], [], 0),
LocationRow('Quest: Vampyre Slayer', 'quest', ['Draynor Village', 'Central Varrock', 'Draynor Manor', ], [], [], 0),
LocationRow('Quest: Imp Catcher', 'quest', ['Wizard Tower', 'Imps', ], [], [], 0),
LocationRow('Quest: Prince Ali Rescue', 'quest', ['Al Kharid', 'Central Varrock', 'Bronze Ores', 'Clay Ore', 'Sheep', 'Spinning Wheel', 'Draynor Village', ], [], [], 0),
LocationRow('Quest: Doric\'s Quest', 'quest', ['Dwarven Mountain Pass', 'Clay Ore', 'Iron Ore', 'Bronze Ores', ], [SkillRequirement('Mining', 15), ], [], 0),
LocationRow('Quest: Black Knights\' Fortress', 'quest', ['Dwarven Mines', 'Falador', 'Monastery', 'Ice Mountain', 'Falador Farms', ], [], ['Progressive Armor', ], 12),
LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0),
LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0),
LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0),
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0),
LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0),
LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0),
LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0),
LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0),
LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16),
LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32),
LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0),
LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2),
LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6),
LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0),
LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0),
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2),
LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6),
LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0),
LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0),
LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0),
LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0),
LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4),
LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8),
LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0),
LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0),
LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0),
LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2),
LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6),
LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0),
LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0),
LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2),
LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6),
LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0),
LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0),
LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2),
LocationRow('Catch a Lobster', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 40), ], [], 6),
LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12),
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2),
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0),
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0),
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0),
LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0),
LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0),
LocationRow('Kill a Barbarian', 'combat', ['Barbarian', ], [SkillRequirement('Combat', 10), ], [], 0),
LocationRow('Kill a Giant Frog', 'combat', ['Lumbridge Swamp', ], [SkillRequirement('Combat', 13), ], [], 0),
LocationRow('Kill a Zombie', 'combat', ['Zombie', ], [SkillRequirement('Combat', 13), ], [], 0),
LocationRow('Kill a Guard', 'combat', ['Guard', ], [SkillRequirement('Combat', 21), ], [], 0),
LocationRow('Kill a Hill Giant', 'combat', ['Hill Giant', ], [SkillRequirement('Combat', 28), ], [], 2),
LocationRow('Kill a Deadly Red Spider', 'combat', ['Deadly Red Spider', ], [SkillRequirement('Combat', 34), ], [], 2),
LocationRow('Kill a Moss Giant', 'combat', ['Moss Giant', ], [SkillRequirement('Combat', 42), ], [], 2),
LocationRow('Kill a Catablepon', 'combat', ['Barbarian Village', ], [SkillRequirement('Combat', 49), ], [], 4),
LocationRow('Kill an Ice Giant', 'combat', ['Ice Giant', ], [SkillRequirement('Combat', 53), ], [], 4),
LocationRow('Kill a Lesser Demon', 'combat', ['Lesser Demon', ], [SkillRequirement('Combat', 82), ], [], 8),
LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8),
LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28),
LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28),
LocationRow('Total XP 5,000', 'general', [], [], [], 0),
LocationRow('Combat Level 5', 'general', [], [], [], 0),
LocationRow('Total XP 10,000', 'general', [], [], [], 0),
LocationRow('Total Level 50', 'general', [], [], [], 0),
LocationRow('Total XP 25,000', 'general', [], [], [], 0),
LocationRow('Total Level 100', 'general', [], [], [], 0),
LocationRow('Total XP 50,000', 'general', [], [], [], 0),
LocationRow('Combat Level 15', 'general', [], [], [], 0),
LocationRow('Total Level 150', 'general', [], [], [], 2),
LocationRow('Total XP 75,000', 'general', [], [], [], 2),
LocationRow('Combat Level 25', 'general', [], [], [], 2),
LocationRow('Total XP 100,000', 'general', [], [], [], 6),
LocationRow('Total Level 200', 'general', [], [], [], 6),
LocationRow('Total XP 125,000', 'general', [], [], [], 6),
LocationRow('Combat Level 30', 'general', [], [], [], 10),
LocationRow('Total Level 250', 'general', [], [], [], 10),
LocationRow('Total XP 150,000', 'general', [], [], [], 10),
LocationRow('Total Level 300', 'general', [], [], [], 16),
LocationRow('Combat Level 40', 'general', [], [], [], 16),
LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0),
LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0),
LocationRow('Points: Demon Slayer', 'points', [], [], [], 0),
LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0),
LocationRow('Points: Romeo & Juliet', 'points', [], [], [], 0),
LocationRow('Points: Sheep Shearer', 'points', [], [], [], 0),
LocationRow('Points: Shield of Arrav', 'points', [], [], [], 0),
LocationRow('Points: Ernest the Chicken', 'points', [], [], [], 0),
LocationRow('Points: Vampyre Slayer', 'points', [], [], [], 0),
LocationRow('Points: Imp Catcher', 'points', [], [], [], 0),
LocationRow('Points: Prince Ali Rescue', 'points', [], [], [], 0),
LocationRow('Points: Doric\'s Quest', 'points', [], [], [], 0),
LocationRow('Points: Black Knights\' Fortress', 'points', [], [], [], 0),
LocationRow('Points: Witch\'s Potion', 'points', [], [], [], 0),
LocationRow('Points: The Knight\'s Sword', 'points', [], [], [], 0),
LocationRow('Points: Goblin Diplomacy', 'points', [], [], [], 0),
LocationRow('Points: Pirate\'s Treasure', 'points', [], [], [], 0),
LocationRow('Points: Rune Mysteries', 'points', [], [], [], 0),
LocationRow('Points: Misthalin Mystery', 'points', [], [], [], 0),
LocationRow('Points: The Corsair Curse', 'points', [], [], [], 0),
LocationRow('Points: X Marks the Spot', 'points', [], [], [], 0),
LocationRow('Points: Below Ice Mountain', 'points', [], [], [], 0),
]

View File

@ -0,0 +1,47 @@
"""
This file was auto generated by LogicCSVToPython.py
"""
from ..Regions import RegionRow
region_rows = [
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]),
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]),
RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]),
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]),
RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]),
RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]),
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]),
RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]),
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]),
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]),
RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]),
RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]),
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]),
RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]),
RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]),
RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]),
RegionRow('Falador East Outskirts', 'Area: Falador', ['Dwarven Mountain Pass', 'Draynor Manor Outskirts', 'Falador Farms', ], ['', ]),
RegionRow('Dwarven Mountain Pass', 'Area: Dwarven Mines', ['Goblin Village', 'Monastery', 'Barbarian Village', 'Falador East Outskirts', 'Falador', ], ['Anvil*', 'Wheat', ]),
RegionRow('Dwarven Mines', 'Area: Dwarven Mines', ['Monastery', 'Ice Mountain', 'Falador', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Gold Ore', 'Anvil', 'Pie Dish', 'Clay Ore', ]),
RegionRow('Goblin Village', 'Area: Ice Mountain', ['Wilderness', 'Dwarven Mountain Pass', ], ['Meat', ]),
RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]),
RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]),
RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]),
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]),
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]),
RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]),
RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]),
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]),
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]),
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]),
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]),
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]),
RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]),
RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]),
RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]),
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]),
RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]),
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]),
RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
]

View File

@ -0,0 +1,54 @@
"""
This file was auto generated by LogicCSVToPython.py
"""
from ..Regions import ResourceRow
resource_rows = [
ResourceRow('Mind Runes'),
ResourceRow('Spinning Wheel'),
ResourceRow('Sheep'),
ResourceRow('Furnace'),
ResourceRow('Chisel'),
ResourceRow('Bronze Ores'),
ResourceRow('Iron Ore'),
ResourceRow('Silver Ore'),
ResourceRow('Coal Ore'),
ResourceRow('Gold Ore'),
ResourceRow('Bronze Anvil'),
ResourceRow('Anvil'),
ResourceRow('Shrimp Spot'),
ResourceRow('Fly Fishing Spot'),
ResourceRow('Lobster Spot'),
ResourceRow('Redberry Bush'),
ResourceRow('Bowl'),
ResourceRow('Meat'),
ResourceRow('Cooking Apple'),
ResourceRow('Pie Dish'),
ResourceRow('Cake Tin'),
ResourceRow('Wheat'),
ResourceRow('Windmill'),
ResourceRow('Egg'),
ResourceRow('Milk'),
ResourceRow('Cheese'),
ResourceRow('Tomato'),
ResourceRow('Oak Tree'),
ResourceRow('Willow Tree'),
ResourceRow('Canoe Tree'),
ResourceRow('Goblin'),
ResourceRow('Barbarian'),
ResourceRow('Zombie'),
ResourceRow('Guard'),
ResourceRow('Hill Giant'),
ResourceRow('Deadly Red Spider'),
ResourceRow('Moss Giant'),
ResourceRow('Ice Giant'),
ResourceRow('Lesser Demon'),
ResourceRow('Rune Essence'),
ResourceRow('Crafting Moulds'),
ResourceRow('Nature Runes'),
ResourceRow('Law Runes'),
ResourceRow('Imps'),
ResourceRow('Clay Ore'),
ResourceRow('Onion'),
ResourceRow('Potato'),
]

212
worlds/osrs/Names.py Normal file
View File

@ -0,0 +1,212 @@
from enum import Enum
class RegionNames(str, Enum):
Lumbridge = "Lumbridge"
Lumbridge_Swamp = "Lumbridge Swamp"
Lumbridge_Farms_East = "Lumbridge Farms East"
Lumbridge_Farms_West = "Lumbridge Farms West"
HAM_Hideout = "HAM Hideout"
Draynor_Village = "Draynor Village"
Draynor_Manor = "Draynor Manor"
Wizards_Tower = "Wizard Tower"
Al_Kharid = "Al Kharid"
Citharede_Abbey = "Citharede Abbey"
South_Of_Varrock = "South of Varrock"
Central_Varrock = "Central Varrock"
Varrock_Palace = "Varrock Palace"
East_Of_Varrock = "East Varrock"
West_Varrock = "West Varrock"
Edgeville = "Edgeville"
Barbarian_Village = "Barbarian Village"
Monastery = "Monastery"
Ice_Mountain = "Ice Mountain"
Dwarven_Mines = "Dwarven Mines"
Falador = "Falador"
Falador_Farm = "Falador Farms"
Crafting_Guild = "Crafting Guild"
Cooks_Guild = "Cook's Guild"
Rimmington = "Rimmington"
Port_Sarim = "Port Sarim"
Mudskipper_Point = "Mudskipper Point"
Karamja = "Karamja"
Corsair_Cove = "Corsair Cove"
Wilderness = "The Wilderness"
Crandor = "Crandor"
# Resource Regions
Egg = "Egg"
Sheep = "Sheep"
Milk = "Milk"
Wheat = "Wheat"
Windmill = "Windmill"
Spinning_Wheel = "Spinning Wheel"
Imp = "Imp"
Bronze_Ores = "Bronze Ores"
Clay_Rock = "Clay Ore"
Coal_Rock = "Coal Ore"
Iron_Rock = "Iron Ore"
Silver_Rock = "Silver Ore"
Gold_Rock = "Gold Ore"
Furnace = "Furnace"
Anvil = "Anvil"
Oak_Tree = "Oak Tree"
Willow_Tree = "Willow Tree"
Shrimp = "Shrimp Spot"
Fly_Fish = "Fly Fishing Spot"
Lobster = "Lobster Spot"
Mind_Runes = "Mind Runes"
Canoe_Tree = "Canoe Tree"
__str__ = str.__str__
class ItemNames(str, Enum):
Lumbridge = "Area: Lumbridge"
Lumbridge_Swamp = "Area: Lumbridge Swamp"
Lumbridge_Farms = "Area: Lumbridge Farms"
HAM_Hideout = "Area: HAM Hideout"
Draynor_Village = "Area: Draynor Village"
Draynor_Manor = "Area: Draynor Manor"
Wizards_Tower = "Area: Wizard Tower"
Al_Kharid = "Area: Al Kharid"
Citharede_Abbey = "Area: Citharede Abbey"
South_Of_Varrock = "Area: South of Varrock"
Central_Varrock = "Area: Central Varrock"
Varrock_Palace = "Area: Varrock Palace"
East_Of_Varrock = "Area: East Varrock"
West_Varrock = "Area: West Varrock"
Edgeville = "Area: Edgeville"
Barbarian_Village = "Area: Barbarian Village"
Monastery = "Area: Monastery"
Ice_Mountain = "Area: Ice Mountain"
Dwarven_Mines = "Area: Dwarven Mines"
Falador = "Area: Falador"
Falador_Farm = "Area: Falador Farms"
Crafting_Guild = "Area: Crafting Guild"
Rimmington = "Area: Rimmington"
Port_Sarim = "Area: Port Sarim"
Mudskipper_Point = "Area: Mudskipper Point"
Karamja = "Area: Karamja"
Crandor = "Area: Crandor"
Corsair_Cove = "Area: Corsair Cove"
Wilderness = "Area: Wilderness"
Progressive_Armor = "Progressive Armor"
Progressive_Weapons = "Progressive Weapons"
Progressive_Tools = "Progressive Tools"
Progressive_Range_Armor = "Progressive Range Armor"
Progressive_Range_Weapon = "Progressive Range Weapon"
Progressive_Magic = "Progressive Magic Spell"
Lobsters = "10 Lobsters"
Swordfish = "5 Swordfish"
Energy_Potions = "10 Energy Potions"
Coins = "5,000 Coins"
Mind_Runes = "50 Mind Runes"
Chaos_Runes = "25 Chaos Runes"
Death_Runes = "10 Death Runes"
Law_Runes = "10 Law Runes"
QP_Cooks_Assistant = "1 QP (Cook's Assistant)"
QP_Demon_Slayer = "3 QP (Demon Slayer)"
QP_Restless_Ghost = "1 QP (The Restless Ghost)"
QP_Romeo_Juliet = "5 QP (Romeo & Juliet)"
QP_Sheep_Shearer = "1 QP (Sheep Shearer)"
QP_Shield_of_Arrav = "1 QP (Shield of Arrav)"
QP_Ernest_the_Chicken = "4 QP (Ernest the Chicken)"
QP_Vampyre_Slayer = "3 QP (Vampyre Slayer)"
QP_Imp_Catcher = "1 QP (Imp Catcher)"
QP_Prince_Ali_Rescue = "3 QP (Prince Ali Rescue)"
QP_Dorics_Quest = "1 QP (Doric's Quest)"
QP_Black_Knights_Fortress = "3 QP (Black Knights' Fortress)"
QP_Witchs_Potion = "1 QP (Witch's Potion)"
QP_Knights_Sword = "1 QP (The Knight's Sword)"
QP_Goblin_Diplomacy = "5 QP (Goblin Diplomacy)"
QP_Pirates_Treasure = "2 QP (Pirate's Treasure)"
QP_Rune_Mysteries = "1 QP (Rune Mysteries)"
QP_Misthalin_Mystery = "1 QP (Misthalin Mystery)"
QP_Corsair_Curse = "2 QP (The Corsair Curse)"
QP_X_Marks_the_Spot = "1 QP (X Marks The Spot)"
QP_Below_Ice_Mountain = "1 QP (Below Ice Mountain)"
__str__ = str.__str__
class LocationNames(str, Enum):
Q_Cooks_Assistant = "Quest: Cook's Assistant"
Q_Demon_Slayer = "Quest: Demon Slayer"
Q_Restless_Ghost = "Quest: The Restless Ghost"
Q_Romeo_Juliet = "Quest: Romeo & Juliet"
Q_Sheep_Shearer = "Quest: Sheep Shearer"
Q_Shield_of_Arrav = "Quest: Shield of Arrav"
Q_Ernest_the_Chicken = "Quest: Ernest the Chicken"
Q_Vampyre_Slayer = "Quest: Vampyre Slayer"
Q_Imp_Catcher = "Quest: Imp Catcher"
Q_Prince_Ali_Rescue = "Quest: Prince Ali Rescue"
Q_Dorics_Quest = "Quest: Doric's Quest"
Q_Black_Knights_Fortress = "Quest: Black Knights' Fortress"
Q_Witchs_Potion = "Quest: Witch's Potion"
Q_Knights_Sword = "Quest: The Knight's Sword"
Q_Goblin_Diplomacy = "Quest: Goblin Diplomacy"
Q_Pirates_Treasure = "Quest: Pirate's Treasure"
Q_Rune_Mysteries = "Quest: Rune Mysteries"
Q_Misthalin_Mystery = "Quest: Misthalin Mystery"
Q_Corsair_Curse = "Quest: The Corsair Curse"
Q_X_Marks_the_Spot = "Quest: X Marks the Spot"
Q_Below_Ice_Mountain = "Quest: Below Ice Mountain"
QP_Cooks_Assistant = "Points: Cook's Assistant"
QP_Demon_Slayer = "Points: Demon Slayer"
QP_Restless_Ghost = "Points: The Restless Ghost"
QP_Romeo_Juliet = "Points: Romeo & Juliet"
QP_Sheep_Shearer = "Points: Sheep Shearer"
QP_Shield_of_Arrav = "Points: Shield of Arrav"
QP_Ernest_the_Chicken = "Points: Ernest the Chicken"
QP_Vampyre_Slayer = "Points: Vampyre Slayer"
QP_Imp_Catcher = "Points: Imp Catcher"
QP_Prince_Ali_Rescue = "Points: Prince Ali Rescue"
QP_Dorics_Quest = "Points: Doric's Quest"
QP_Black_Knights_Fortress = "Points: Black Knights' Fortress"
QP_Witchs_Potion = "Points: Witch's Potion"
QP_Knights_Sword = "Points: The Knight's Sword"
QP_Goblin_Diplomacy = "Points: Goblin Diplomacy"
QP_Pirates_Treasure = "Points: Pirate's Treasure"
QP_Rune_Mysteries = "Points: Rune Mysteries"
QP_Misthalin_Mystery = "Points: Misthalin Mystery"
QP_Corsair_Curse = "Points: The Corsair Curse"
QP_X_Marks_the_Spot = "Points: X Marks the Spot"
QP_Below_Ice_Mountain = "Points: Below Ice Mountain"
Guppy = "Prepare a Guppy"
Cavefish = "Prepare a Cavefish"
Tetra = "Prepare a Tetra"
Barronite_Deposit = "Crush a Barronite Deposit"
Oak_Log = "Cut an Oak Log"
Willow_Log = "Cut a Willow Log"
Catch_Lobster = "Catch a Lobster"
Mine_Silver = "Mine Silver"
Mine_Coal = "Mine Coal"
Mine_Gold = "Mine Gold"
Smelt_Silver = "Smelt a Silver Bar"
Smelt_Steel = "Smelt a Steel Bar"
Smelt_Gold = "Smelt a Gold Bar"
Cut_Sapphire = "Cut a Sapphire"
Cut_Emerald = "Cut an Emerald"
Cut_Ruby = "Cut a Ruby"
Cut_Diamond = "Cut a Diamond"
K_Lesser_Demon = "Kill a Lesser Demon"
K_Ogress_Shaman = "Kill an Ogress Shaman"
Bake_Apple_Pie = "Bake an Apple Pie"
Bake_Cake = "Bake a Cake"
Bake_Meat_Pizza = "Bake a Meat Pizza"
Total_XP_5000 = "5,000 Total XP"
Total_XP_10000 = "10,000 Total XP"
Total_XP_25000 = "25,000 Total XP"
Total_XP_50000 = "50,000 Total XP"
Total_XP_100000 = "100,000 Total XP"
Total_Level_50 = "Total Level 50"
Total_Level_100 = "Total Level 100"
Total_Level_150 = "Total Level 150"
Total_Level_200 = "Total Level 200"
Combat_Level_5 = "Combat Level 5"
Combat_Level_15 = "Combat Level 15"
Combat_Level_25 = "Combat Level 25"
Travel_on_a_Canoe = "Travel on a Canoe"
Q_Dragon_Slayer = "Quest: Dragon Slayer"
__str__ = str.__str__

474
worlds/osrs/Options.py Normal file
View File

@ -0,0 +1,474 @@
from dataclasses import dataclass
from Options import Choice, Toggle, Range, PerGameCommonOptions
MAX_COMBAT_TASKS = 16
MAX_PRAYER_TASKS = 3
MAX_MAGIC_TASKS = 4
MAX_RUNECRAFT_TASKS = 3
MAX_CRAFTING_TASKS = 5
MAX_MINING_TASKS = 5
MAX_SMITHING_TASKS = 4
MAX_FISHING_TASKS = 5
MAX_COOKING_TASKS = 5
MAX_FIREMAKING_TASKS = 2
MAX_WOODCUTTING_TASKS = 3
NON_QUEST_LOCATION_COUNT = 22
class StartingArea(Choice):
"""
Which chunks are available at the start. The player may need to move through locked chunks to reach the starting
area, but any areas that require quests, skills, or coins are not available as a starting location.
"Any Bank" rolls a random region that contains a bank.
Chunksanity can start you in any chunk. Hope you like woodcutting!
"""
display_name = "Starting Region"
option_lumbridge = 0
option_al_kharid = 1
option_varrock_east = 2
option_varrock_west = 3
option_edgeville = 4
option_falador = 5
option_draynor = 6
option_wilderness = 7
option_any_bank = 8
option_chunksanity = 9
default = 0
class BrutalGrinds(Toggle):
"""
Whether to allow skill tasks without having reasonable access to the usual skill training path.
For example, if enabled, you could be forced to train smithing without an anvil purely by smelting bars,
or training fishing to high levels entirely on shrimp.
"""
display_name = "Allow Brutal Grinds"
class ProgressiveTasks(Toggle):
"""
Whether skill tasks should always be generated in order of easiest to hardest.
If enabled, you would not be assigned "Mine Gold" without also being assigned
"Mine Silver", "Mine Coal", and "Mine Iron". Enabling this will result in a generally shorter seed, but with
a lower variety of tasks.
"""
display_name = "Progressive Tasks"
class MaxCombatLevel(Range):
"""
The highest combat level of monster to possibly be assigned as a task.
If set to 0, no combat tasks will be generated.
"""
range_start = 0
range_end = 1520
default = 50
class MaxCombatTasks(Range):
"""
The maximum number of Combat Tasks to possibly be assigned.
If set to 0, no combat tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_COMBAT_TASKS
default = MAX_COMBAT_TASKS
class CombatTaskWeight(Range):
"""
How much to favor generating combat tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxPrayerLevel(Range):
"""
The highest Prayer requirement of any task generated.
If set to 0, no Prayer tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxPrayerTasks(Range):
"""
The maximum number of Prayer Tasks to possibly be assigned.
If set to 0, no Prayer tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_PRAYER_TASKS
default = MAX_PRAYER_TASKS
class PrayerTaskWeight(Range):
"""
How much to favor generating Prayer tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxMagicLevel(Range):
"""
The highest Magic requirement of any task generated.
If set to 0, no Magic tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxMagicTasks(Range):
"""
The maximum number of Magic Tasks to possibly be assigned.
If set to 0, no Magic tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_MAGIC_TASKS
default = MAX_MAGIC_TASKS
class MagicTaskWeight(Range):
"""
How much to favor generating Magic tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxRunecraftLevel(Range):
"""
The highest Runecraft requirement of any task generated.
If set to 0, no Runecraft tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxRunecraftTasks(Range):
"""
The maximum number of Runecraft Tasks to possibly be assigned.
If set to 0, no Runecraft tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_RUNECRAFT_TASKS
default = MAX_RUNECRAFT_TASKS
class RunecraftTaskWeight(Range):
"""
How much to favor generating Runecraft tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxCraftingLevel(Range):
"""
The highest Crafting requirement of any task generated.
If set to 0, no Crafting tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxCraftingTasks(Range):
"""
The maximum number of Crafting Tasks to possibly be assigned.
If set to 0, no Crafting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_CRAFTING_TASKS
default = MAX_CRAFTING_TASKS
class CraftingTaskWeight(Range):
"""
How much to favor generating Crafting tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxMiningLevel(Range):
"""
The highest Mining requirement of any task generated.
If set to 0, no Mining tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxMiningTasks(Range):
"""
The maximum number of Mining Tasks to possibly be assigned.
If set to 0, no Mining tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_MINING_TASKS
default = MAX_MINING_TASKS
class MiningTaskWeight(Range):
"""
How much to favor generating Mining tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxSmithingLevel(Range):
"""
The highest Smithing requirement of any task generated.
If set to 0, no Smithing tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxSmithingTasks(Range):
"""
The maximum number of Smithing Tasks to possibly be assigned.
If set to 0, no Smithing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_SMITHING_TASKS
default = MAX_SMITHING_TASKS
class SmithingTaskWeight(Range):
"""
How much to favor generating Smithing tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxFishingLevel(Range):
"""
The highest Fishing requirement of any task generated.
If set to 0, no Fishing tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxFishingTasks(Range):
"""
The maximum number of Fishing Tasks to possibly be assigned.
If set to 0, no Fishing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_FISHING_TASKS
default = MAX_FISHING_TASKS
class FishingTaskWeight(Range):
"""
How much to favor generating Fishing tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxCookingLevel(Range):
"""
The highest Cooking requirement of any task generated.
If set to 0, no Cooking tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxCookingTasks(Range):
"""
The maximum number of Cooking Tasks to possibly be assigned.
If set to 0, no Cooking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_COOKING_TASKS
default = MAX_COOKING_TASKS
class CookingTaskWeight(Range):
"""
How much to favor generating Cooking tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxFiremakingLevel(Range):
"""
The highest Firemaking requirement of any task generated.
If set to 0, no Firemaking tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxFiremakingTasks(Range):
"""
The maximum number of Firemaking Tasks to possibly be assigned.
If set to 0, no Firemaking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_FIREMAKING_TASKS
default = MAX_FIREMAKING_TASKS
class FiremakingTaskWeight(Range):
"""
How much to favor generating Firemaking tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MaxWoodcuttingLevel(Range):
"""
The highest Woodcutting requirement of any task generated.
If set to 0, no Woodcutting tasks will be generated.
"""
range_start = 0
range_end = 99
default = 50
class MaxWoodcuttingTasks(Range):
"""
The maximum number of Woodcutting Tasks to possibly be assigned.
If set to 0, no Woodcutting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
range_start = 0
range_end = MAX_WOODCUTTING_TASKS
default = MAX_WOODCUTTING_TASKS
class WoodcuttingTaskWeight(Range):
"""
How much to favor generating Woodcutting tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
class MinimumGeneralTasks(Range):
"""
How many guaranteed general progression tasks to be assigned (total level, total XP, etc.).
General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so
there is no maximum.
"""
range_start = 0
range_end = NON_QUEST_LOCATION_COUNT
default = 10
class GeneralTaskWeight(Range):
"""
How much to favor generating General tasks over other types of task.
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
range_start = 0
range_end = 99
default = 50
@dataclass
class OSRSOptions(PerGameCommonOptions):
starting_area: StartingArea
brutal_grinds: BrutalGrinds
progressive_tasks: ProgressiveTasks
max_combat_level: MaxCombatLevel
max_combat_tasks: MaxCombatTasks
combat_task_weight: CombatTaskWeight
max_prayer_level: MaxPrayerLevel
max_prayer_tasks: MaxPrayerTasks
prayer_task_weight: PrayerTaskWeight
max_magic_level: MaxMagicLevel
max_magic_tasks: MaxMagicTasks
magic_task_weight: MagicTaskWeight
max_runecraft_level: MaxRunecraftLevel
max_runecraft_tasks: MaxRunecraftTasks
runecraft_task_weight: RunecraftTaskWeight
max_crafting_level: MaxCraftingLevel
max_crafting_tasks: MaxCraftingTasks
crafting_task_weight: CraftingTaskWeight
max_mining_level: MaxMiningLevel
max_mining_tasks: MaxMiningTasks
mining_task_weight: MiningTaskWeight
max_smithing_level: MaxSmithingLevel
max_smithing_tasks: MaxSmithingTasks
smithing_task_weight: SmithingTaskWeight
max_fishing_level: MaxFishingLevel
max_fishing_tasks: MaxFishingTasks
fishing_task_weight: FishingTaskWeight
max_cooking_level: MaxCookingLevel
max_cooking_tasks: MaxCookingTasks
cooking_task_weight: CookingTaskWeight
max_firemaking_level: MaxFiremakingLevel
max_firemaking_tasks: MaxFiremakingTasks
firemaking_task_weight: FiremakingTaskWeight
max_woodcutting_level: MaxWoodcuttingLevel
max_woodcutting_tasks: MaxWoodcuttingTasks
woodcutting_task_weight: WoodcuttingTaskWeight
minimum_general_tasks: MinimumGeneralTasks
general_task_weight: GeneralTaskWeight

12
worlds/osrs/Regions.py Normal file
View File

@ -0,0 +1,12 @@
import typing
class RegionRow(typing.NamedTuple):
name: str
itemReq: str
connections: typing.List[str]
resources: typing.List[str]
class ResourceRow(typing.NamedTuple):
name: str

657
worlds/osrs/__init__.py Normal file
View File

@ -0,0 +1,657 @@
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):
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, world: MultiWorld, player: int):
super().__init__(world, 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
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
return data
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 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":
item = [item for item in item_rows if item.name == item_name][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

View File

@ -0,0 +1,114 @@
# Old School Runescape
## What is the Goal of this Randomizer?
The goal is to complete the quest "Dragon Slayer I" with limited access to gear and map chunks while following normal
Ironman/Group Ironman restrictions on a fresh free-to-play account.
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file. OSRS contains many options for a highly customizable experience. The options available to you are:
* **Starting Area** - The starting region of your run. This is the first region you will have available, and you can always
freely return to it (see the section below for when it is allowed to cross locked regions to access it)
* You may select a starting city from the list of Lumbridge, Al Kharid, Varrock (East or West), Edgeville, Falador,
Draynor Village, or The Wilderness (Ferox Enclave)
* The option "Any Bank" will choose one of the above regions at random
* The option "Chunksanity" can start you in _any_ chunk, regardless of whether it has access to a bank.
* **Brutal Grinds** - If enabled, the logic will assume you are willing to go to great lengths to train skills.
* As an example, when enabled, it might be in logic to obtain tin and copper from mob drops and smelt bronze bars to
reach Smithing Level 40 to smelt gold for a task.
* If left disabled, the logic will always ensure you have a reasonable method for training a skill to reach a specific
task, such as having access to intermediate-level training options
* **Progressive Tasks** - If enabled, tasks for a skill are generated in order from earliest to latest.
* For example, your first Smithing task would always be "Smelt an Iron Bar", then "Smelt a Silver Bar", and so on.
You would never have the task "Smelt a Gold Bar" without having every previous Smithing task as well.
This can lead to a more consistent length of run, and is generally shorter than disabling it, but with less variety.
* **Skill Category Weighting Options**
* These are available in each task category (all trainable skills plus "Combat" and "General")
* **Max [Category] Level** - The highest level you intend to have to reach in order to complete all tasks for this
category. For the Combat category, this is the max level of monster you are willing to fight.
General tasks do not have a level and thus do not have this option.
* **Max [Category] Tasks** - The highest number of tasks in this category you are willing to be assigned.
Note that you can end up with _less_ than this amount, but never more. The "General" category is used to fill remaining
spots so a maximum is not specified, instead it has a _minimum_ count.
* **[Category] Task Weighting** - The relative weighting of this category to all of the others. Increase this to make
tasks in this category more likely.
## What does randomization do to this game?
The OSRS Archipelago Randomizer takes the form of a "Chunkman" account, a form of challenge account
where you are limited to specific regions of the map (known as "chunks") until you complete tasks to unlock
more. The plugin will interface with the [Region Locker Plugin](https://github.com/slaytostay/region-locker) to
visually display these chunk borders and highlight them as locked or unlocked. The optional included GPU plugin for the
Region Locker can tint the locked areas gray, but is incompatible with other GPU plugins such as 117's HD OSRS.
If you choose not to include it, the world map will show locked and unlocked regions instead.
In order to access a region, you will need to access it entirely through unlocked regions. At no point are you
ever allowed to cross through locked regions, with the following exceptions:
* If your starting region is not Lumbridge, when you complete Tutorial Island, you will need to traverse locked regions
to reach your intended starting location.
* If your starting region is not Lumbridge, you are allowed to "Home Teleport" to your starting region by using the
Lumbridge Home Teleport Spell and then walking to your start location. This is to prevent you from getting "stuck" after
using one-way transportation such as the Port Sarim Jail Teleport from Shantay Pass and being locked out of progression.
* All of your starting Tutorial Island items are assumed to be available at all times. If you have lost an important
item such as a Tinderbox, and cannot re-obtain it in your unlocked region, you are allowed to enter locked regions to
replace it in the least obtrusive way possible.
* If you need to adjust Group Ironman settings, such as adding or removing a member, you may freely access The Node
to do so.
When passing through locked regions for such exceptions, do not interact with any NPCs, items, or enemies and attempt
to spend as little time in them as possible.
The plugin will prevent equipping items that you have not unlocked the ability to wield. For example, attempting
to equip an Iron Platebody before the first Progressive Armor unlock will display a chat message and will not
equip the item.
The plugin will show a list of your current tasks in the sidebar. The plugin will be able to detect the completion
of most tasks, but in the case that a task cannot be detected (for example, killing an enemy with no
drop table such as Deadly Red Spiders), the task can be marked as complete manually by clicking
on the button. This button can also be used to mark completed tasks you have done while playing OSRS mobile or
on a different client without having the plugin available. Simply click the button the next time you are logged in to
Runelite and connected to send the check.
Due to the nature of randomizing a live MMO with no ability to freely edit the character or adjust game logic or
balancing, this randomizer relies heavily on **the honor system**. The plugin cannot prevent you from walking through
locked regions or equipping locked items with the plugin disabled before connecting. It is important
to acknowledge before starting that the entire purpose of the randomizer is a self-imposed challenge, and there
is little point in cheating by circumventing the plugin's restrictions or marking a task complete without actually
completing it. If you wish to play OSRS with no restrictions, that is always available without the plugin.
In order to access the AP Text Client commands (such as `!hint` or to chat with other players in the seed), enter your
command in chat prefaced by the string `!ap`. Example commands:
`!ap buying gf 100k` -> Sends the message "buying gf 100k" to the server
`!ap !hint Area: Lumbridge` -> Attempts to hint for the "Area: Lumbridge" item. Results will appear in your chat box.
Other server messages, such as chat, will appear in your chat box, prefaced by the Archipelago icon.
## What items and locations get shuffled?
Items:
- Every map region (at least one chunk but sometimes more)
- Weapon tiers from iron to Rune (bronze is available from the start)
- Armor tiers from iron to Rune (bronze is available from the start)
- Two Spell Tiers (bolt and blast spells)
- Three tiers of Ranged Armor (leather, studded leather + vambraces, green dragonhide)
- Three tiers of Ranged Weapons (oak, willow, maple bows and their respective highest tier of arrows)
Locations:
* Every Quest is a location that will always be included in every seed
* A random assortment of tasks, separated into categories based on the skill required.
These task categories can have different weights, minimums, and maximums based on your options.
* For a full list of Locations, items, and regions, see the
[Logic Document](https://docs.google.com/spreadsheets/d/1R8Cm8L6YkRWeiN7uYrdru8Vc1DlJ0aFAinH_fwhV8aU/edit?usp=sharing)
## Which items can be in another player's world?
Any item or region unlock can be found in any player's world.
## What does another world's item look like in Old School Runescape?
Upon completing a task, the item and recipient will be listed in the player's chatbox.
## When the player receives an item, what happens?
In addition to the message appearing in the chatbox, a UI window will appear listing the item and who sent it.
These boxes also appear when connecting to a seed already in progress to list the items you have acquired while offline.
The sidebar will list all received items below the task list, starting with regions, then showing the highest tier of
equipment in each category.

View File

@ -0,0 +1,58 @@
# Setup Guide for Old School Runescape
## Required Software
- [RuneLite](https://runelite.net/)
- If the account being used has been migrated to a Jagex Account, the [Jagex Launcher](https://www.jagex.com/en-GB/launcher)
will also be necessary to run RuneLite
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
You can customize your settings by visiting the
[Old School Runescape Player Options Page](/games/Old%20School%20Runescape/player-options).
## Joining a MultiWorld Game
### Install the RuneLite Plugins
Open RuneLite and click on the wrench icon on the right side. From there, click on the plug icon to access the
Plugin Hub. You will need to install the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago)
and [Region Locker Plugin](https://github.com/slaytostay/region-locker). The Region Locker plugin
will include three plugins; only the `Region Locker` plugin itself is required. The `Region Locker GPU` plugin can be
used to display locked chunks in gray, but is incompatible with other GPU plugins such as 117's HD OSRS and can be
disabled.
### Create a new OSRS Account
The OSRS Randomizer assumes you are playing on a newly created f2p Ironman account. As such, you will need to [create a
new Runescape account](https://secure.runescape.com/m=account-creation/create_account?theme=oldschool).
If you already have a [Jagex Account](https://www.jagex.com/en-GB/accounts) you can add up to 20 characters on
one account through the Jagex Launcher. Note that there is currently no way to _remove_ characters
from a Jagex Account, as such, you might want to create a separate account to hold your Archipelago
characters if you intend to use your main Jagex account for more characters in the future.
**Protip**: In order to avoid having to remember random email addresses for many accounts, take advantage of an email
alias, a feature supported by most email providers. Any text after a `+` in your email address will redirect to your
normal address, but the email will be recognized by the Jagex login as a new email address. For example, if your email
were `Archipelago@gmail.com`, entering `Archipelago+OSRSRandomizer@gmail.com` would cause the confirmation email to
be sent to your primary address, but the alias can be used to create a new account. One recommendation would be to
include the date of generation in the account, such as `Archipelago+APYYMMDD@gmail.com` for easy memorability.
After creating an account, you may run through Tutorial Island without connecting; the randomizer has no
effect on the Tutorial.
### Connect to the Multiserver
In the Archipelago Plugin, enter your server information. The `Auto Reconnect on Login For` field should remain blank;
it will be populated by the character name you first connect with, and it will reconnect to the AP server whenever that
character logs in. Open the Archipelago panel on the right-hand side to connect to the multiworld while logged in to
a game world to associate this character to the randomizer.
For further information about how to connect to the server in the RuneLite plugin,
please see the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) instructions.