diff --git a/README.md b/README.md index cebd4f7e752..5b66e3db878 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index ab841e65ee4..4f012c306be 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -115,6 +115,9 @@ # Ocarina of Time /worlds/oot/ @espeon65536 +# Old School Runescape +/worlds/osrs @digiholic + # Overcooked! 2 /worlds/overcooked2/ @toasterparty diff --git a/worlds/osrs/Items.py b/worlds/osrs/Items.py new file mode 100644 index 00000000000..0679c964e77 --- /dev/null +++ b/worlds/osrs/Items.py @@ -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', +} diff --git a/worlds/osrs/Locations.py b/worlds/osrs/Locations.py new file mode 100644 index 00000000000..b5827d60f2f --- /dev/null +++ b/worlds/osrs/Locations.py @@ -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" diff --git a/worlds/osrs/LogicCSV/LogicCSVToPython.py b/worlds/osrs/LogicCSV/LogicCSVToPython.py new file mode 100644 index 00000000000..ed8bd8172a0 --- /dev/null +++ b/worlds/osrs/LogicCSV/LogicCSVToPython.py @@ -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") diff --git a/worlds/osrs/LogicCSV/items_generated.py b/worlds/osrs/LogicCSV/items_generated.py new file mode 100644 index 00000000000..b5e610a6e3a --- /dev/null +++ b/worlds/osrs/LogicCSV/items_generated.py @@ -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), +] diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py new file mode 100644 index 00000000000..073e505ad8f --- /dev/null +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -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), +] diff --git a/worlds/osrs/LogicCSV/regions_generated.py b/worlds/osrs/LogicCSV/regions_generated.py new file mode 100644 index 00000000000..87b3747d938 --- /dev/null +++ b/worlds/osrs/LogicCSV/regions_generated.py @@ -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', ]), +] diff --git a/worlds/osrs/LogicCSV/resources_generated.py b/worlds/osrs/LogicCSV/resources_generated.py new file mode 100644 index 00000000000..18c2ebe2f31 --- /dev/null +++ b/worlds/osrs/LogicCSV/resources_generated.py @@ -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'), +] diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py new file mode 100644 index 00000000000..95aed742b6f --- /dev/null +++ b/worlds/osrs/Names.py @@ -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__ diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py new file mode 100644 index 00000000000..520cd8e8b06 --- /dev/null +++ b/worlds/osrs/Options.py @@ -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 diff --git a/worlds/osrs/Regions.py b/worlds/osrs/Regions.py new file mode 100644 index 00000000000..436cdf3c7c7 --- /dev/null +++ b/worlds/osrs/Regions.py @@ -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 diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py new file mode 100644 index 00000000000..f726b4b81bf --- /dev/null +++ b/worlds/osrs/__init__.py @@ -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 diff --git a/worlds/osrs/docs/en_Old School Runescape.md b/worlds/osrs/docs/en_Old School Runescape.md new file mode 100644 index 00000000000..d367082b227 --- /dev/null +++ b/worlds/osrs/docs/en_Old School Runescape.md @@ -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. \ No newline at end of file diff --git a/worlds/osrs/docs/setup_en.md b/worlds/osrs/docs/setup_en.md new file mode 100644 index 00000000000..47c1c8f16fd --- /dev/null +++ b/worlds/osrs/docs/setup_en.md @@ -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. \ No newline at end of file