diff --git a/worlds/osrs/Locations.py b/worlds/osrs/Locations.py index b5827d60f2..324da86be4 100644 --- a/worlds/osrs/Locations.py +++ b/worlds/osrs/Locations.py @@ -3,6 +3,8 @@ import typing from BaseClasses import Location +task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] + class SkillRequirement(typing.NamedTuple): skill: str level: int diff --git a/worlds/osrs/LogicCSV/LogicCSVToPython.py b/worlds/osrs/LogicCSV/LogicCSVToPython.py index b66f53cc9d..082bda7a08 100644 --- a/worlds/osrs/LogicCSV/LogicCSVToPython.py +++ b/worlds/osrs/LogicCSV/LogicCSVToPython.py @@ -8,7 +8,7 @@ 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 = "v2.0.4" +data_csv_tag = "v2.0.5" # If true, generate using file names in the repository debug = False diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py index 4c1cd0bdd8..03156b1c71 100644 --- a/worlds/osrs/LogicCSV/locations_generated.py +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -77,7 +77,7 @@ location_rows = [ 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', 32), ], [], 2), - LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0), + LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [SkillRequirement('Cooking', 32), ], [], 0), 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 a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0), diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py index 55a040b095..cf0754a3c2 100644 --- a/worlds/osrs/Options.py +++ b/worlds/osrs/Options.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from Options import Choice, Toggle, Range, PerGameCommonOptions -MAX_COMBAT_TASKS = 16 +MAX_COMBAT_TASKS = 17 MAX_PRAYER_TASKS = 5 MAX_MAGIC_TASKS = 7 diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py index 7fd770f0f7..1cdaabe3e2 100644 --- a/worlds/osrs/Rules.py +++ b/worlds/osrs/Rules.py @@ -190,6 +190,8 @@ def get_firemaking_skill_rule(level, player, options) -> CollectionRule: def get_skill_rule(skill, level, player, options) -> CollectionRule: + if level <= 1: + return lambda state: True if skill.lower() == "fishing": return get_fishing_skill_rule(level, player, options) if skill.lower() == "mining": diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index a54e272d05..e0587daff3 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -1,11 +1,11 @@ import typing -from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState -from Fill import fill_restrictive, FillError +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld from worlds.AutoWorld import WebWorld, World +from Options import OptionError from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ chunksanity_special_region_names -from .Locations import OSRSLocation, LocationRow +from .Locations import OSRSLocation, LocationRow, task_types from .Rules import * from .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames @@ -47,6 +47,7 @@ class OSRSWorld(World): base_id = 0x070000 data_version = 1 explicit_indirect_conditions = False + ut_can_gen_without_yaml = True 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))} @@ -105,6 +106,18 @@ class OSRSWorld(World): # Set Starting Chunk self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + elif hasattr(self.multiworld,"re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough: + re_gen_passthrough = self.multiworld.re_gen_passthrough[self.game] # UT passthrough + if "starting_area" in re_gen_passthrough: + self.starting_area_item = re_gen_passthrough["starting_area"] + for task_type in task_types: + if f"max_{task_type}_level" in re_gen_passthrough: + getattr(self.options,f"max_{task_type}_level").value = re_gen_passthrough[f"max_{task_type}_level"] + max_count = getattr(self.options,f"max_{task_type}_tasks") + max_count.value = max_count.range_end + self.options.brutal_grinds.value = re_gen_passthrough["brutal_grinds"] + + """ This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. @@ -115,20 +128,13 @@ class OSRSWorld(World): data = self.options.as_dict("brutal_grinds") data["data_csv_tag"] = data_csv_tag data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv + for task_type in task_types: + data[f"max_{task_type}_level"] = getattr(self.options,f"max_{task_type}_level").value return data - def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: - if "starting_area" in slot_data: - self.starting_area_item = slot_data["starting_area"] - menu_region = self.multiworld.get_region("Menu",self.player) - menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot - if self.starting_area_item in chunksanity_special_region_names: - starting_area_region = chunksanity_special_region_names[self.starting_area_item] - else: - starting_area_region = self.starting_area_item[6:] # len("Area: ") - starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") - starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) - starting_entrance.connect(self.region_name_to_data[starting_area_region]) + @staticmethod + def interpret_slot_data(slot_data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + return slot_data def create_regions(self) -> None: """ @@ -195,6 +201,8 @@ class OSRSWorld(World): generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override locations_required = 0 for item_row in item_rows: + if item_row.name == self.starting_area_item: + continue #skip starting area # If it's a filler item, set it aside for later if item_row.progression == ItemClassification.filler: continue @@ -206,15 +214,18 @@ class OSRSWorld(World): locations_required += item_row.amount if self.options.enable_duds: locations_required += self.options.dud_count - locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 - + locations_added = 0 # Keep track of the number of locations we add so we don't add more the number of items we're going to make # Quests are always added first, before anything else is rolled for i, location_row in enumerate(location_rows): - if location_row.category in {"quest", "points", "goal"}: + if location_row.category in {"quest"}: if self.task_within_skill_levels(location_row.skills): self.create_and_add_location(i) - if location_row.category == "quest": - locations_added += 1 + locations_added += 1 + elif location_row.category in {"goal"}: + if not self.task_within_skill_levels(location_row.skills): + raise OptionError(f"Goal location for {self.player_name} not allowed in skill levels") #it doesn't actually have any, but just in case for future + self.create_and_add_location(i) + # Build up the weighted Task Pool rnd = self.random @@ -225,18 +236,28 @@ class OSRSWorld(World): rnd.shuffle(general_tasks) else: general_tasks.reverse() - for i in range(self.options.minimum_general_tasks): + general_tasks_added = 0 + while general_tasks_added0: + task = general_tasks.pop() + if self.task_within_skill_levels(task.skills): + self.add_location(task) + locations_added += 1 + general_tasks_added += 1 + if general_tasks_added < self.options.minimum_general_tasks: + raise OptionError(f"{self.plyaer_name} doesn't have enough general tasks to create required minimum count"+ + f", raise maximum skill levels or lower minimum general tasks") - general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 + general_weight = self.options.general_task_weight.value 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_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] @@ -263,10 +284,13 @@ class OSRSWorld(World): 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: + if general_weight > 0 and len(general_tasks)>0: all_tasks.append(general_tasks) all_weights.append(general_weight) + if not generation_is_fake and locations_added > locations_required: #due to minimum general tasks we already have more than needed + raise OptionError(f"Too many locations created for {self.player_name}, lower the minimum general tasks") + 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] @@ -282,9 +306,9 @@ class OSRSWorld(World): del all_tasks[index] del all_weights[index] - else: + else: # We can ignore general tasks in UT because they will have been cleared already if len(general_tasks) == 0: - raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + + raise OptionError(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) @@ -296,7 +320,7 @@ class OSRSWorld(World): self.create_and_add_location(index) def create_items(self) -> None: - filler_items = [] + filler_items:list[ItemRow] = [] for item_row in item_rows: if item_row.name != self.starting_area_item: # If it's a filler item, set it aside for later @@ -321,7 +345,7 @@ class OSRSWorld(World): def get_filler_item_name(self) -> str: if self.options.enable_duds: - return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler]) + return self.random.choice([item.name for item in item_rows if item.progression == ItemClassification.filler]) else: return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor, @@ -388,6 +412,12 @@ class OSRSWorld(World): # Set the access rule for the QP Location add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) + qp = 0 + for qp_event in self.available_QP_locations: + qp += int(qp_event[0]) + if qp < self.location_rows_by_name[LocationNames.Q_Dragon_Slayer].qp: + raise OptionError(f"{self.player_name} doesn't have enough quests for reach goal, increase maximum skill levels") + # 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"))