diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py deleted file mode 100644 index 448e3272aef8..000000000000 --- a/worlds/ror2/Items.py +++ /dev/null @@ -1,194 +0,0 @@ -from BaseClasses import Item -from .Options import ItemWeights -from .RoR2Environments import * - - -class RiskOfRainItem(Item): - game: str = "Risk of Rain 2" - - -# 37000 - 37699, 38000 -item_table: Dict[str, int] = { - "Dio's Best Friend": 37001, - "Common Item": 37002, - "Uncommon Item": 37003, - "Legendary Item": 37004, - "Boss Item": 37005, - "Lunar Item": 37006, - "Equipment": 37007, - "Item Scrap, White": 37008, - "Item Scrap, Green": 37009, - "Item Scrap, Red": 37010, - "Item Scrap, Yellow": 37011, - "Void Item": 37012, - "Beads of Fealty": 37013 -} - -# 37700 - 37699 -################################################## -# environments - -environment_offest = 37700 - -# add ALL environments into the item table -environment_offset_table = shift_by_offset(environment_ALL_table, environment_offest) -item_table.update(shift_by_offset(environment_ALL_table, environment_offest)) -# use the sotv dlc in the item table so that all names can be looked up regardless of use - -# end of environments -################################################## - -default_weights: Dict[str, int] = { - "Item Scrap, Green": 16, - "Item Scrap, Red": 4, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 32, - "Common Item": 64, - "Uncommon Item": 32, - "Legendary Item": 8, - "Boss Item": 4, - "Lunar Item": 16, - "Void Item": 16, - "Equipment": 32 -} - -new_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 75, - "Uncommon Item": 40, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 20 -} - -uncommon_weights: Dict[str, int] = { - "Item Scrap, Green": 45, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 45, - "Uncommon Item": 100, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -legendary_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 50, - "Uncommon Item": 25, - "Legendary Item": 100, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -lunartic_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 100, - "Void Item": 0, - "Equipment": 0 -} - -chaos_weights: Dict[str, int] = { - "Item Scrap, Green": 80, - "Item Scrap, Red": 45, - "Item Scrap, Yellow": 30, - "Item Scrap, White": 100, - "Common Item": 100, - "Uncommon Item": 70, - "Legendary Item": 30, - "Boss Item": 20, - "Lunar Item": 60, - "Void Item": 60, - "Equipment": 40 -} - -no_scraps_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 100, - "Uncommon Item": 40, - "Legendary Item": 15, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 25 -} - -even_weights: Dict[str, int] = { - "Item Scrap, Green": 1, - "Item Scrap, Red": 1, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 1, - "Common Item": 1, - "Uncommon Item": 1, - "Legendary Item": 1, - "Boss Item": 1, - "Lunar Item": 1, - "Void Item": 1, - "Equipment": 1 -} - -scraps_only: Dict[str, int] = { - "Item Scrap, Green": 70, - "Item Scrap, White": 100, - "Item Scrap, Red": 30, - "Item Scrap, Yellow": 5, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 0, - "Equipment": 0 -} - -void_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 100, - "Equipment": 0 -} - -item_pool_weights: Dict[int, Dict[str, int]] = { - ItemWeights.option_default: default_weights, - ItemWeights.option_new: new_weights, - ItemWeights.option_uncommon: uncommon_weights, - ItemWeights.option_legendary: legendary_weights, - ItemWeights.option_lunartic: lunartic_weights, - ItemWeights.option_chaos: chaos_weights, - ItemWeights.option_no_scraps: no_scraps_weights, - ItemWeights.option_even: even_weights, - ItemWeights.option_scraps_only: scraps_only, - ItemWeights.option_void: void_weights, -} - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py deleted file mode 100644 index 7db3ceca73b3..000000000000 --- a/worlds/ror2/Locations.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Tuple -from BaseClasses import Location -from .Options import TotalLocations -from .Options import ChestsPerEnvironment -from .Options import ShrinesPerEnvironment -from .Options import ScavengersPerEnvironment -from .Options import ScannersPerEnvironment -from .Options import AltarsPerEnvironment -from .RoR2Environments import * - - -class RiskOfRainLocation(Location): - game: str = "Risk of Rain 2" - - -ror2_locations_start_id = 38000 - - -def get_classic_item_pickups(n: int) -> Dict[str, int]: - """Get n ItemPickups, capped at the max value for TotalLocations""" - n = max(n, 0) - n = min(n, TotalLocations.range_end) - return { f"ItemPickup{i+1}": ror2_locations_start_id+i for i in range(n) } - - -item_pickups = get_classic_item_pickups(TotalLocations.range_end) -location_table = item_pickups - - -def environment_abreviation(long_name:str) -> str: - """convert long environment names to initials""" - abrev = "" - # go through every word finding a letter (or number) for an initial - for word in long_name.split(): - initial = word[0] - for letter in word: - if letter.isalnum(): - initial = letter - break - abrev+= initial - return abrev - -# highest numbered orderedstages (this is so we can treat the easily caculate the check ids based on the environment and location "offset") -highest_orderedstage: int= max(compress_dict_list_horizontal(environment_orderedstages_table).values()) - -ror2_locations_start_orderedstage = ror2_locations_start_id + TotalLocations.range_end - -class orderedstage_location: - """A class to behave like a struct for storing the offsets of location types in the allocated space per orderedstage environments.""" - # TODO is there a better, more generic way to do this? - offset_ChestsPerEnvironment = 0 - offset_ShrinesPerEnvironment = offset_ChestsPerEnvironment + ChestsPerEnvironment.range_end - offset_ScavengersPerEnvironment = offset_ShrinesPerEnvironment + ShrinesPerEnvironment.range_end - offset_ScannersPerEnvironment = offset_ScavengersPerEnvironment + ScavengersPerEnvironment.range_end - offset_AltarsPerEnvironment = offset_ScannersPerEnvironment + ScannersPerEnvironment.range_end - - # total space allocated to the locations in a single orderedstage environment - allocation = offset_AltarsPerEnvironment + AltarsPerEnvironment.range_end - - def get_environment_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, environment: Tuple[str, int]) -> Dict[str, int]: - """Get the locations within a specific environment""" - environment_name = environment[0] - environment_index = environment[1] - locations = {} - - # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers - # TODO perhaps a hashing algorithm could be used to compress this range and save "wasted" ids - environment_start_id = environment_index * orderedstage_location.allocation + ror2_locations_start_orderedstage - for n in range(chests): - locations.update({f"{environment_name}: Chest {n+1}": n + orderedstage_location.offset_ChestsPerEnvironment + environment_start_id}) - for n in range(shrines): - locations.update({f"{environment_name}: Shrine {n+1}": n + orderedstage_location.offset_ShrinesPerEnvironment + environment_start_id}) - for n in range(scavengers): - locations.update({f"{environment_name}: Scavenger {n+1}": n + orderedstage_location.offset_ScavengersPerEnvironment + environment_start_id}) - for n in range(scanners): - locations.update({f"{environment_name}: Radio Scanner {n+1}": n + orderedstage_location.offset_ScannersPerEnvironment + environment_start_id}) - for n in range(altars): - locations.update({f"{environment_name}: Newt Altar {n+1}": n + orderedstage_location.offset_AltarsPerEnvironment + environment_start_id}) - return locations - - def get_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, dlc_sotv:bool) -> Dict[str, int]: - """Get a dictionary of locations for the ordedstage environments with the locations from the parameters.""" - locations = {} - orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) - if(dlc_sotv): orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) - # for every environment, generate the respective locations - for environment_name, environment_index in orderedstages.items(): - # locations = locations | orderedstage_location.get_environment_locations( - locations.update(orderedstage_location.get_environment_locations( - chests=chests, - shrines=shrines, - scavengers=scavengers, - scanners=scanners, - altars=altars, - environment=(environment_name, environment_index) - )) - return locations - - def getall_locations(dlc_sotv:bool=True) -> Dict[str, int]: - """ - Get all locations in ordered stages. - Set dlc_sotv to true for the SOTV DLC to be included. - """ - # to get all locations, attempt using as many locations as possible - return orderedstage_location.get_locations( - chests=ChestsPerEnvironment.range_end, - shrines=ShrinesPerEnvironment.range_end, - scavengers=ScavengersPerEnvironment.range_end, - scanners=ScannersPerEnvironment.range_end, - altars=AltarsPerEnvironment.range_end, - dlc_sotv=dlc_sotv - ) - - -ror2_location_post_orderedstage = ror2_locations_start_orderedstage + highest_orderedstage*orderedstage_location.allocation -location_table.update(orderedstage_location.getall_locations()) -# use the sotv dlc in the lookup table so that all ids can be looked up regardless of use - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in location_table.items()} diff --git a/worlds/ror2/RoR2Environments.py b/worlds/ror2/RoR2Environments.py deleted file mode 100644 index 2a9bf73e9805..000000000000 --- a/worlds/ror2/RoR2Environments.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Dict, List, TypeVar - -# TODO probably move to Locations - -environment_vanilla_orderedstage_1_table: Dict[str, int] = { - "Distant Roost": 7, # blackbeach - "Distant Roost (2)": 8, # blackbeach2 - "Titanic Plains": 15, # golemplains - "Titanic Plains (2)": 16, # golemplains2 -} -environment_vanilla_orderedstage_2_table: Dict[str, int] = { - "Abandoned Aqueduct": 17, # goolake - "Wetland Aspect": 12, # foggyswamp -} -environment_vanilla_orderedstage_3_table: Dict[str, int] = { - "Rallypoint Delta": 13, # frozenwall - "Scorched Acres": 47, # wispgraveyard -} -environment_vanilla_orderedstage_4_table: Dict[str, int] = { - "Abyssal Depths": 10, # dampcavesimple - "Siren's Call": 37, # shipgraveyard - "Sundered Grove": 35, # rootjungle -} -environment_vanilla_orderedstage_5_table: Dict[str, int] = { - "Sky Meadow": 38, # skymeadow -} - -environment_vanilla_hidden_realm_table: Dict[str, int] = { - "Hidden Realm: Bulwark's Ambry": 5, # artifactworld - "Hidden Realm: Bazaar Between Time": 6, # bazaar - "Hidden Realm: Gilded Coast": 14, # goldshores - "Hidden Realm: A Moment, Whole": 27, # limbo - "Hidden Realm: A Moment, Fractured": 33, # mysteryspace -} - -environment_vanilla_special_table: Dict[str, int] = { - "Void Fields": 4, # arena - "Commencement": 32, # moon2 -} - -environment_sotv_orderedstage_1_table: Dict[str, int] = { - "Siphoned Forest": 39, # snowyforest -} -environment_sotv_orderedstage_2_table: Dict[str, int] = { - "Aphelian Sanctuary": 3, # ancientloft -} -environment_sotv_orderedstage_3_table: Dict[str, int] = { - "Sulfur Pools": 41, # sulfurpools -} -environment_sotv_orderedstage_4_table: Dict[str, int] = { } -environment_sotv_orderedstage_5_table: Dict[str, int] = { } - -# TODO idk much and idc much about simulacrum, is there a forced order or something? -environment_sotv_simulacrum_table: Dict[str, int] = { - "The Simulacrum (Aphelian Sanctuary)": 20, # itancientloft - "The Simulacrum (Abyssal Depths)": 21, # itdampcave - "The Simulacrum (Rallypoint Delta)": 22, # itfrozenwall - "The Simulacrum (Titanic Plains)": 23, # itgolemplains - "The Simulacrum (Abandoned Aqueduct)": 24, # itgoolake - "The Simulacrum (Commencement)": 25, # itmoon - "The Simulacrum (Sky Meadow)": 26, # itskymeadow -} - -environment_sotv_special_table: Dict[str, int] = { - "Void Locus": 46, # voidstage - "The Planetarium": 45, # voidraid -} - -X = TypeVar("X") -Y = TypeVar("Y") - - -def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: - """Combine all dictionaries in a list together into one dictionary.""" - compressed: Dict[X,Y] = {} - for individual in list_of_dict: compressed.update(individual) - return compressed - -def collapse_dict_list_vertical(list_of_dict1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: - """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" - # find the length of the longest list - length = len(list_of_dict1) - for list_of_dictN in args: - length = max(length, len(list_of_dictN)) - - # create a combined list with a length the same as the longest list - collapsed = [{}] * (length) - # The reason the list_of_dict1 is not directly used to make collapsed is - # side effects can occur if all the dictionaries are not manually unioned. - - # merge contents from list_of_dict1 - for i in range(len(list_of_dict1)): - collapsed[i] = {**collapsed[i], **list_of_dict1[i]} - - # merge contents of remaining lists_of_dicts - for list_of_dictN in args: - for i in range(len(list_of_dictN)): - collapsed[i] = {**collapsed[i], **list_of_dictN[i]} - - return collapsed - -# TODO potentially these should only be created when they are directly referenced (unsure of the space/time cost of creating these initially) - -environment_vanilla_orderedstages_table = [ environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, environment_vanilla_orderedstage_5_table ] -environment_vanilla_table = {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} - -environment_sotv_orderedstages_table = [ environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table, environment_sotv_orderedstage_4_table, environment_sotv_orderedstage_5_table ] -environment_sotv_non_simulacrum_table = {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} -environment_sotv_table = {**environment_sotv_non_simulacrum_table} - -environment_non_orderedstages_table = {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_simulacrum_table, **environment_sotv_special_table} -environment_orderedstages_table = collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) -environment_ALL_table = {**environment_vanilla_table, **environment_sotv_table} - - -def shift_by_offset(dictionary: Dict[str, int], offset:int) -> Dict[str, int]: - """Shift all indexes in a dictionary by an offset""" - return {name:index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 22c65dd9deb7..8735ce81fd5d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,14 +1,16 @@ import string -from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest -from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location -from .Rules import set_rules -from .RoR2Environments import * - -from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ItemWeights, ROR2Options +from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler_table, environment_offset +from .locations import RiskOfRainLocation, item_pickups, get_locations +from .rules import set_rules +from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset + +from BaseClasses import Item, ItemClassification, Tutorial +from .options import ItemWeights, ROR2Options from worlds.AutoWorld import World, WebWorld -from .Regions import create_regions +from .regions import create_explore_regions, create_classic_regions +from typing import List, Dict, Any class RiskOfWeb(WebWorld): @@ -18,7 +20,7 @@ class RiskOfWeb(WebWorld): "English", "setup_en.md", "setup/en", - ["Ijwu"] + ["Ijwu", "Kindasneaki"] )] @@ -32,38 +34,53 @@ class RiskOfRainWorld(World): options_dataclass = ROR2Options options: ROR2Options topology_present = False - - item_name_to_id = item_table + item_name_to_id = {name: data.code for name, data in item_table.items()} + item_name_groups = { + "Stages": {name for name, data in item_table.items() if data.category == "Stage"}, + "Environments": {name for name, data in item_table.items() if data.category == "Environment"}, + "Upgrades": {name for name, data in item_table.items() if data.category == "Upgrade"}, + "Fillers": {name for name, data in item_table.items() if data.category == "Filler"}, + "Traps": {name for name, data in item_table.items() if data.category == "Trap"}, + } location_name_to_id = item_pickups - data_version = 7 - required_client_version = (0, 4, 2) + data_version = 8 + required_client_version = (0, 4, 4) web = RiskOfWeb() total_revivals: int - def __init__(self, multiworld: "MultiWorld", player: int): - super().__init__(multiworld, player) - self.junk_pool: Dict[str, int] = {} - def generate_early(self) -> None: # figure out how many revivals should exist in the pool if self.options.goal == "classic": total_locations = self.options.total_locations.value else: total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * total_locations) if self.options.start_with_revive: self.total_revivals -= 1 + if self.options.victory == "voidling" and not self.options.dlc_sotv: + self.options.victory.value = self.options.victory.option_any + + def create_regions(self) -> None: + + if self.options.goal == "classic": + # classic mode + create_classic_regions(self) + else: + # explore mode + create_explore_regions(self) + + self.create_events() def create_items(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend @@ -77,25 +94,26 @@ def create_items(self) -> None: # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table if self.options.dlc_sotv: - environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sotv_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offest) + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) if self.options.dlc_sotv: - environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest) + environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} environments_to_precollect = 5 if self.options.begin_with_loop else 1 # percollect environments for each stage (or just stage 1) for i in range(environments_to_precollect): - unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) + unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) environments_pool.pop(unlock[0]) # Generate item pool - itempool: List = [] + itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] # Add revive items for the player itempool += ["Dio's Best Friend"] * self.total_revivals - itempool += ["Beads of Fealty"] for env_name, _ in environments_pool.items(): itempool += [env_name] @@ -105,38 +123,28 @@ def create_items(self) -> None: total_locations = self.options.total_locations.value else: # explore mode + # Add Stage items for logic gates + itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"] total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) # Create junk items - self.junk_pool = self.create_junk_pool() + junk_pool = self.create_junk_pool() # Fill remaining items with randomly generated junk - while len(itempool) < total_locations: - itempool.append(self.get_filler_item_name()) + filler = self.random.choices(*zip(*junk_pool.items()), k=total_locations - len(itempool)) + itempool.extend(filler) # Convert itempool into real items - itempool = list(map(lambda name: self.create_item(name), itempool)) - self.multiworld.itempool += itempool + self.multiworld.itempool += map(self.create_item, itempool) - def set_rules(self) -> None: - set_rules(self.multiworld, self.player) - - def get_filler_item_name(self) -> str: - if not self.junk_pool: - self.junk_pool = self.create_junk_pool() - weights = [data for data in self.junk_pool.values()] - filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights, - k=1)[0] - return filler - - def create_junk_pool(self) -> Dict: + def create_junk_pool(self) -> Dict[str, int]: # if presets are enabled generate junk_pool from the selected preset pool_option = self.options.item_weights.value junk_pool: Dict[str, int] = {} @@ -144,7 +152,7 @@ def create_junk_pool(self) -> Dict: # generate chaos weights if the preset is chosen if pool_option == ItemWeights.option_chaos: for name, max_value in item_pool_weights[pool_option].items(): - junk_pool[name] = self.multiworld.random.randint(0, max_value) + junk_pool[name] = self.random.randint(0, max_value) else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets @@ -159,10 +167,22 @@ def create_junk_pool(self) -> Dict: "Boss Item": self.options.boss_item.value, "Lunar Item": self.options.lunar_item.value, "Void Item": self.options.void_item.value, - "Equipment": self.options.equipment.value + "Equipment": self.options.equipment.value, + "Money": self.options.money.value, + "Lunar Coin": self.options.lunar_coin.value, + "1000 Exp": self.options.experience.value, + "Mountain Trap": self.options.mountain_trap.value, + "Time Warp Trap": self.options.time_warp_trap.value, + "Combat Trap": self.options.combat_trap.value, + "Teleport Trap": self.options.teleport_trap.value, } - - # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled + # remove trap items from the pool (excluding lunar items) + if not self.options.enable_trap: + junk_pool.pop("Mountain Trap") + junk_pool.pop("Time Warp Trap") + junk_pool.pop("Combat Trap") + junk_pool.pop("Teleport Trap") + # remove lunar items from the pool if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic): junk_pool.pop("Lunar Item") # remove void items from the pool @@ -171,98 +191,58 @@ def create_junk_pool(self) -> Dict: return junk_pool - def create_regions(self) -> None: - - if self.options.goal == "classic": - # classic mode - menu = create_region(self.multiworld, self.player, "Menu") - self.multiworld.regions.append(menu) - # By using a victory region, we can define it as being connected to by several regions - # which can then determine the availability of the victory. - victory_region = create_region(self.multiworld, self.player, "Victory") - self.multiworld.regions.append(victory_region) - petrichor = create_region(self.multiworld, self.player, "Petrichor V", - get_classic_item_pickups(self.options.total_locations.value)) - self.multiworld.regions.append(petrichor) - - # classic mode can get to victory from the beginning of the game - to_victory = Entrance(self.player, "beating game", petrichor) - petrichor.exits.append(to_victory) - to_victory.connect(victory_region) + def create_item(self, name: str) -> Item: + data = item_table[name] + return RiskOfRainItem(name, data.item_type, data.code, self.player) - connection = Entrance(self.player, "Lobby", menu) - menu.exits.append(connection) - connection.connect(petrichor) - else: - # explore mode - create_regions(self.multiworld, self.player) + def set_rules(self) -> None: + set_rules(self) - create_events(self.multiworld, self.player) + def get_filler_item_name(self) -> str: + weights = [data.weight for data in filler_table.values()] + filler = self.multiworld.random.choices([filler for filler in filler_table.keys()], weights, + k=1)[0] + return filler - def fill_slot_data(self): - options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations", - "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", - "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", - "final_stage_death", "death_link", casing="camel") + def fill_slot_data(self) -> Dict[str, Any]: + options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations", + "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", + "scanner_per_stage", "altars_per_stage", "total_revivals", + "start_with_revive", "final_stage_death", "death_link", + casing="camel") return { **options_dict, - "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), + "seed": "".join(self.random.choice(string.digits) for _ in range(16)), + "offset": offset } - def create_item(self, name: str) -> Item: - item_id = item_table[name] - classification = ItemClassification.filler - if name in {"Dio's Best Friend", "Beads of Fealty"}: - classification = ItemClassification.progression - elif name in {"Legendary Item", "Boss Item"}: - classification = ItemClassification.useful - elif name == "Lunar Item": - classification = ItemClassification.trap - - # Only check for an item to be a environment unlock if those are known to be in the pool. - # This should shave down comparisons. - - elif name in environment_ALL_table.keys(): - if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}: - classification = ItemClassification.useful - else: - classification = ItemClassification.progression - - item = RiskOfRainItem(name, classification, item_id, self.player) - return item - - -def create_events(world: MultiWorld, player: int) -> None: - total_locations = world.worlds[player].options.total_locations.value - num_of_events = total_locations // 25 - if total_locations / 25 == num_of_events: - num_of_events -= 1 - world_region = world.get_region("Petrichor V", player) - if world.worlds[player].options.goal == "classic": - # only setup Pickups when using classic_mode - for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) - event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) - event_loc.access_rule = \ - lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player) - world_region.locations.append(event_loc) - elif world.worlds[player].options.goal == "explore": - for n in range(1, 6): - - event_region = world.get_region(f"OrderedStage_{n}", player) - event_loc = RiskOfRainLocation(player, f"Stage_{n}", None, event_region) - event_loc.place_locked_item(RiskOfRainItem(f"Stage_{n}", ItemClassification.progression, None, player)) + def create_events(self) -> None: + total_locations = self.options.total_locations.value + num_of_events = total_locations // 25 + if total_locations / 25 == num_of_events: + num_of_events -= 1 + world_region = self.multiworld.get_region("Petrichor V", self.player) + if self.options.goal == "classic": + # classic mode + # only setup Pickups when using classic_mode + for i in range(num_of_events): + event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region) + event_loc.place_locked_item( + RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, + self.player)) + event_loc.access_rule = \ + lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", self.player) + world_region.locations.append(event_loc) + else: + # explore mode + event_region = self.multiworld.get_region("OrderedStage_5", self.player) + event_loc = RiskOfRainLocation(self.player, "Stage 5", None, event_region) + event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player)) event_loc.show_in_spoiler = False event_region.locations.append(event_loc) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) - victory_region = world.get_region("Victory", player) - victory_event = RiskOfRainLocation(player, "Victory", None, victory_region) - victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) - world_region.locations.append(victory_event) - - -def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: - ret = Region(name, player, world) - for location_name, location_id in locations.items(): - ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) - return ret + victory_region = self.multiworld.get_region("Victory", self.player) + victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player)) + victory_region.locations.append(victory_event) diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 4e59d2bf4157..0fa99c071b9c 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -55,4 +55,15 @@ the player's YAML. You can talk to other in the multiworld chat using the RoR2 chat. All other multiworld remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can also optionally connect to the multiworld using the text client, which can be found in the -[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). \ No newline at end of file +[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). + +### In-Game Commands +These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". + - `archipelago_deathlink true/false` Toggle deathlink. + - `archipelago_disconnect` Disconnect from AP. + - `archipelago_final_stage_death true/false` Toggle final stage death. + +Explore Mode only + - `archipelago_show_unlocked_stages` Show which stages have been received. + - `archipelago_highlight_satellite true/false` This will highlight the satellite to make it easier to see (Default false). \ No newline at end of file diff --git a/worlds/ror2/items.py b/worlds/ror2/items.py new file mode 100644 index 000000000000..449686d04bf0 --- /dev/null +++ b/worlds/ror2/items.py @@ -0,0 +1,309 @@ +from BaseClasses import Item, ItemClassification +from .options import ItemWeights +from .ror2environments import environment_all_table +from typing import NamedTuple, Optional, Dict + + +class RiskOfRainItem(Item): + game: str = "Risk of Rain 2" + + +class RiskOfRainItemData(NamedTuple): + category: str + code: int + item_type: ItemClassification = ItemClassification.filler + weight: Optional[int] = None + + +offset: int = 37000 +filler_offset: int = offset + 300 +trap_offset: int = offset + 400 +stage_offset: int = offset + 500 +environment_offset: int = offset + 700 +# Upgrade item ids 37002 - 37012 +upgrade_table: Dict[str, RiskOfRainItemData] = { + "Common Item": RiskOfRainItemData("Upgrade", 2 + offset, ItemClassification.filler, 64), + "Uncommon Item": RiskOfRainItemData("Upgrade", 3 + offset, ItemClassification.filler, 32), + "Legendary Item": RiskOfRainItemData("Upgrade", 4 + offset, ItemClassification.useful, 8), + "Boss Item": RiskOfRainItemData("Upgrade", 5 + offset, ItemClassification.useful, 4), + "Equipment": RiskOfRainItemData("Upgrade", 7 + offset, ItemClassification.filler, 32), + "Item Scrap, White": RiskOfRainItemData("Upgrade", 8 + offset, ItemClassification.filler, 32), + "Item Scrap, Green": RiskOfRainItemData("Upgrade", 9 + offset, ItemClassification.filler, 16), + "Item Scrap, Red": RiskOfRainItemData("Upgrade", 10 + offset, ItemClassification.filler, 4), + "Item Scrap, Yellow": RiskOfRainItemData("Upgrade", 11 + offset, ItemClassification.filler, 1), + "Void Item": RiskOfRainItemData("Upgrade", 12 + offset, ItemClassification.filler, 16), +} +# Other item ids 37001, 37013-37014 +other_table: Dict[str, RiskOfRainItemData] = { + "Dio's Best Friend": RiskOfRainItemData("ExtraLife", 1 + offset, ItemClassification.progression_skip_balancing), + "Beads of Fealty": RiskOfRainItemData("Beads", 13 + offset, ItemClassification.progression), + "Radar Scanner": RiskOfRainItemData("Radar", 14 + offset, ItemClassification.useful), +} +# Filler item ids 37301 - 37303 +filler_table: Dict[str, RiskOfRainItemData] = { + "Money": RiskOfRainItemData("Filler", 1 + filler_offset, ItemClassification.filler, 64), + "Lunar Coin": RiskOfRainItemData("Filler", 2 + filler_offset, ItemClassification.filler, 20), + "1000 Exp": RiskOfRainItemData("Filler", 3 + filler_offset, ItemClassification.filler, 40), +} +# Trap item ids 37401 - 37404 (Lunar items used to be part of the upgrade item list, so keeping the id the same) +trap_table: Dict[str, RiskOfRainItemData] = { + "Lunar Item": RiskOfRainItemData("Trap", 6 + offset, ItemClassification.trap, 16), + "Mountain Trap": RiskOfRainItemData("Trap", 1 + trap_offset, ItemClassification.trap, 5), + "Time Warp Trap": RiskOfRainItemData("Trap", 2 + trap_offset, ItemClassification.trap, 20), + "Combat Trap": RiskOfRainItemData("Trap", 3 + trap_offset, ItemClassification.trap, 20), + "Teleport Trap": RiskOfRainItemData("Trap", 4 + trap_offset, ItemClassification.trap, 10), +} +# Stage item ids 37501 - 37504 +stage_table: Dict[str, RiskOfRainItemData] = { + "Stage 1": RiskOfRainItemData("Stage", 1 + stage_offset, ItemClassification.progression), + "Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression), + "Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression), + "Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression), + +} + +item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table} +# Environment item ids 37700 - 37746 +################################################## +# environments + + +# add ALL environments into the item table +def create_environment_table(name: str, environment_id: int, environment_classification: ItemClassification) \ + -> Dict[str, RiskOfRainItemData]: + return {name: RiskOfRainItemData("Environment", environment_offset + environment_id, environment_classification)} + + +environment_table: Dict[str, RiskOfRainItemData] = {} +# use the sotv dlc in the item table so that all names can be looked up regardless of use +for data, key in environment_all_table.items(): + classification = ItemClassification.progression + if data in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast"}: + classification = ItemClassification.useful + environment_table.update(create_environment_table(data, key, classification)) + +item_table.update(environment_table) + +# end of environments +################################################## + +default_weights: Dict[str, int] = { + "Item Scrap, Green": 16, + "Item Scrap, Red": 4, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 32, + "Common Item": 64, + "Uncommon Item": 32, + "Legendary Item": 8, + "Boss Item": 4, + "Void Item": 16, + "Equipment": 32, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +new_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 75, + "Uncommon Item": 40, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +uncommon_weights: Dict[str, int] = { + "Item Scrap, Green": 45, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 45, + "Uncommon Item": 100, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +legendary_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 50, + "Uncommon Item": 25, + "Legendary Item": 100, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +chaos_weights: Dict[str, int] = { + "Item Scrap, Green": 80, + "Item Scrap, Red": 45, + "Item Scrap, Yellow": 30, + "Item Scrap, White": 100, + "Common Item": 100, + "Uncommon Item": 70, + "Legendary Item": 30, + "Boss Item": 20, + "Void Item": 60, + "Equipment": 40, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +no_scraps_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 100, + "Uncommon Item": 40, + "Legendary Item": 15, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 25, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +even_weights: Dict[str, int] = { + "Item Scrap, Green": 1, + "Item Scrap, Red": 1, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 1, + "Common Item": 1, + "Uncommon Item": 1, + "Legendary Item": 1, + "Boss Item": 1, + "Void Item": 1, + "Equipment": 1, + "Money": 1, + "Lunar Coin": 1, + "1000 Exp": 1, + "Lunar Item": 1, + "Mountain Trap": 1, + "Time Warp Trap": 1, + "Combat Trap": 1, + "Teleport Trap": 1 +} + +scraps_only: Dict[str, int] = { + "Item Scrap, Green": 70, + "Item Scrap, White": 100, + "Item Scrap, Red": 30, + "Item Scrap, Yellow": 5, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +lunartic_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 100, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +void_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 100, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} + +item_pool_weights: Dict[int, Dict[str, int]] = { + ItemWeights.option_default: default_weights, + ItemWeights.option_new: new_weights, + ItemWeights.option_uncommon: uncommon_weights, + ItemWeights.option_legendary: legendary_weights, + ItemWeights.option_chaos: chaos_weights, + ItemWeights.option_no_scraps: no_scraps_weights, + ItemWeights.option_even: even_weights, + ItemWeights.option_scraps_only: scraps_only, + ItemWeights.option_lunartic: lunartic_weights, + ItemWeights.option_void: void_weights, +} diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py new file mode 100644 index 000000000000..13077b3e149c --- /dev/null +++ b/worlds/ror2/locations.py @@ -0,0 +1,89 @@ +from typing import Dict +from BaseClasses import Location +from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \ + ScannersPerEnvironment, AltarsPerEnvironment +from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table + + +class RiskOfRainLocation(Location): + game: str = "Risk of Rain 2" + + +ror2_locations_start_id = 38000 + + +def get_classic_item_pickups(n: int) -> Dict[str, int]: + """Get n ItemPickups, capped at the max value for TotalLocations""" + n = max(n, 0) + n = min(n, TotalLocations.range_end) + return {f"ItemPickup{i + 1}": ror2_locations_start_id + i for i in range(n)} + + +item_pickups = get_classic_item_pickups(TotalLocations.range_end) +location_table = item_pickups + +# this is so we can easily calculate the environment and location "offset" ids +ror2_locations_start_ordered_stage = ror2_locations_start_id + TotalLocations.range_end + +# TODO is there a better, more generic way to do this? +offset_chests = 0 +offset_shrines = offset_chests + ChestsPerEnvironment.range_end +offset_scavengers = offset_shrines + ShrinesPerEnvironment.range_end +offset_scanners = offset_scavengers + ScavengersPerEnvironment.range_end +offset_altars = offset_scanners + ScannersPerEnvironment.range_end + +# total space allocated to the locations in a single orderedstage environment +allocation = offset_altars + AltarsPerEnvironment.range_end + + +def get_environment_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, + environment_name: str, environment_index: int) -> Dict[str, int]: + """Get the locations within a specific environment""" + locations = {} + + # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers + environment_start_id = environment_index * allocation + ror2_locations_start_ordered_stage + for n in range(chests): + locations.update({f"{environment_name}: Chest {n + 1}": n + offset_chests + environment_start_id}) + for n in range(shrines): + locations.update({f"{environment_name}: Shrine {n + 1}": n + offset_shrines + environment_start_id}) + for n in range(scavengers): + locations.update({f"{environment_name}: Scavenger {n + 1}": n + offset_scavengers + environment_start_id}) + for n in range(scanners): + locations.update({f"{environment_name}: Radio Scanner {n + 1}": n + offset_scanners + environment_start_id}) + for n in range(altars): + locations.update({f"{environment_name}: Newt Altar {n + 1}": n + offset_altars + environment_start_id}) + return locations + + +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ + -> Dict[str, int]: + """Get a dictionary of locations for the orderedstage environments with the locations from the parameters.""" + locations = {} + orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) + if dlc_sotv: + orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + # for every environment, generate the respective locations + for environment_name, environment_index in orderedstages.items(): + locations.update(get_environment_locations( + chests=chests, + shrines=shrines, + scavengers=scavengers, + scanners=scanners, + altars=altars, + environment_name=environment_name, + environment_index=environment_index), + ) + return locations + + +# Get all locations in ordered stages. +location_table.update(get_locations( + chests=ChestsPerEnvironment.range_end, + shrines=ShrinesPerEnvironment.range_end, + scavengers=ScavengersPerEnvironment.range_end, + scanners=ScannersPerEnvironment.range_end, + altars=AltarsPerEnvironment.range_end, + dlc_sotv=True, +)) diff --git a/worlds/ror2/Options.py b/worlds/ror2/options.py similarity index 73% rename from worlds/ror2/Options.py rename to worlds/ror2/options.py index 0ed0a87b17d6..7daf8a844666 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/options.py @@ -4,7 +4,7 @@ # NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks # Be careful when changing the range_end values not to go into another game's IDs -# NOTE that these changes to range_end must also be reflected in the RoR2 client so it understands the same ids. +# NOTE that these changes to range_end must also be reflected in the RoR2 client, so it understands the same ids. class Goal(Choice): """ @@ -19,6 +19,21 @@ class Goal(Choice): default = 1 +class Victory(Choice): + """ + Mithrix: Defeat Mithrix in Commencement + Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Any: Any victory in the game will count. See Final Stage Death for additional ways. + """ + display_name = "Victory Condition" + option_any = 0 + option_mithrix = 1 + option_voidling = 2 + option_limbo = 3 + default = 0 + + class TotalLocations(Range): """Classic Mode: Number of location checks which are added to the Risk of Rain playthrough.""" display_name = "Total Locations" @@ -100,6 +115,11 @@ class ShrineUseStep(Range): default = 0 +class AllowTrapItems(Toggle): + """Allows Trap items in the item pool.""" + display_name = "Enable Trap Items" + + class AllowLunarItems(DefaultOnToggle): """Allows Lunar items in the item pool.""" display_name = "Enable Lunar Item Shuffling" @@ -111,10 +131,14 @@ class StartWithRevive(DefaultOnToggle): class FinalStageDeath(Toggle): - """The following will count as a win if set to true: + """The following will count as a win if set to "true", and victory is set to "any": Dying in Commencement. Dying in The Planetarium. - Obliterating yourself""" + Obliterating yourself + If not use the following to tell if final stage death will count: + Victory: mithrix - only dying in Commencement will count. + Victory: voidling - only dying in The Planetarium will count. + Victory: limbo - Obliterating yourself will count.""" display_name = "Final Stage Death is Win" @@ -247,6 +271,76 @@ class Equipment(Range): default = 32 +class Money(Range): + """Weight of money items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Money" + range_start = 0 + range_end = 100 + default = 64 + + +class LunarCoin(Range): + """Weight of lunar coin items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Lunar Coins" + range_start = 0 + range_end = 100 + default = 20 + + +class Experience(Range): + """Weight of 1000 exp items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "1000 Exp" + range_start = 0 + range_end = 100 + default = 40 + + +class MountainTrap(Range): + """Weight of mountain trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Mountain Trap" + range_start = 0 + range_end = 100 + default = 5 + + +class TimeWarpTrap(Range): + """Weight of time warp trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Time Warp Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class CombatTrap(Range): + """Weight of combat trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Combat Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class TeleportTrap(Range): + """Weight of teleport trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Teleport Trap" + range_start = 0 + range_end = 100 + default = 20 + + class ItemPoolPresetToggle(Toggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" display_name = "Use Item Weight Presets" @@ -258,28 +352,30 @@ class ItemWeights(Choice): - New is a test for a potential adjustment to the default weights. - Uncommon puts a large number of uncommon items in the pool. - Legendary puts a large number of legendary items in the pool. - - Lunartic makes everything a lunar item. - - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy. + - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being + too easy. - No Scraps removes all scrap items from the item pool. - Even generates the item pool with every item having an even weight. - Scraps Only will be only scrap items in the item pool. + - Lunartic makes everything a lunar item. - Void makes everything a void item.""" display_name = "Item Weights" option_default = 0 option_new = 1 option_uncommon = 2 option_legendary = 3 - option_lunartic = 4 - option_chaos = 5 - option_no_scraps = 6 - option_even = 7 - option_scraps_only = 8 + option_chaos = 4 + option_no_scraps = 5 + option_even = 6 + option_scraps_only = 7 + option_lunartic = 8 option_void = 9 @dataclass class ROR2Options(PerGameCommonOptions): goal: Goal + victory: Victory total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment shrines_per_stage: ShrinesPerEnvironment @@ -294,6 +390,7 @@ class ROR2Options(PerGameCommonOptions): death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep + enable_trap: AllowTrapItems enable_lunar: AllowLunarItems item_weights: ItemWeights item_pool_presets: ItemPoolPresetToggle @@ -309,3 +406,10 @@ class ROR2Options(PerGameCommonOptions): lunar_item: LunarItem void_item: VoidItem equipment: Equipment + money: Money + lunar_coin: LunarCoin + experience: Experience + mountain_trap: MountainTrap + time_warp_trap: TimeWarpTrap + combat_trap: CombatTrap + teleport_trap: TeleportTrap diff --git a/worlds/ror2/Regions.py b/worlds/ror2/regions.py similarity index 59% rename from worlds/ror2/Regions.py rename to worlds/ror2/regions.py index 94f5aaf71ee8..13b229da9249 100644 --- a/worlds/ror2/Regions.py +++ b/worlds/ror2/regions.py @@ -1,7 +1,10 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import MultiWorld, Region, Entrance -from .Locations import location_table, RiskOfRainLocation +from BaseClasses import Region, Entrance, MultiWorld +from .locations import location_table, RiskOfRainLocation, get_classic_item_pickups + +if TYPE_CHECKING: + from . import RiskOfRainWorld class RoRRegionData(NamedTuple): @@ -9,10 +12,14 @@ class RoRRegionData(NamedTuple): region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld # Default Locations non_dlc_regions: Dict[str, RoRRegionData] = { - "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", "Titanic Plains", "Titanic Plains (2)"]), + "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", + "Titanic Plains", "Titanic Plains (2)"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), @@ -34,33 +41,36 @@ def create_regions(multiworld: MultiWorld, player: int): } other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), - "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", "Commencement"]), + "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", + "Commencement"]), "OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time", - "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", "Wetland Aspect"]), + "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", + "Wetland Aspect"]), "OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]), - "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", "Sundered Grove"]), + "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", + "Sundered Grove"]), "OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]), "Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]), - "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory"]), + "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Fields": RoRRegionData(None, []), "Victory": RoRRegionData(None, None), - "Petrichor V": RoRRegionData(None, ["Victory"]), + "Petrichor V": RoRRegionData(None, []), "Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None), "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } dlc_other_regions: Dict[str, RoRRegionData] = { - "The Planetarium": RoRRegionData(None, ["Victory"]), + "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } # Totals of each item - chests = int(multiworld.chests_per_stage[player]) - shrines = int(multiworld.shrines_per_stage[player]) - scavengers = int(multiworld.scavengers_per_stage[player]) - scanners = int(multiworld.scanner_per_stage[player]) - newt = int(multiworld.altars_per_stage[player]) + chests = int(ror2_options.chests_per_stage) + shrines = int(ror2_options.shrines_per_stage) + scavengers = int(ror2_options.scavengers_per_stage) + scanners = int(ror2_options.scanner_per_stage) + newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: all_location_regions = {**non_dlc_regions, **dlc_regions} # Locations @@ -88,23 +98,35 @@ def create_regions(multiworld: MultiWorld, player: int): regions_pool: Dict = {**all_location_regions, **other_regions} # DLC Locations - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: non_dlc_regions["Menu"].region_exits.append("Siphoned Forest") other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary") other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools") other_regions["Void Fields"].region_exits.append("Void Locus") + other_regions["Commencement"].region_exits.append("The Planetarium") regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions} + # Check to see if Victory needs to be removed from regions + if ror2_options.victory == "mithrix": + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + elif ror2_options.victory == "voidling": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + elif ror2_options.victory == "limbo": + other_regions["Commencement"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + # Create all the regions for name, data in regions_pool.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) + multiworld.regions.append(create_explore_region(multiworld, player, name, data)) # Connect all the regions to their exits for name, data in regions_pool.items(): create_connections_in_regions(multiworld, player, name, data) -def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_explore_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> Region: region = Region(name, player, multiworld) if data.locations: for location_name in data.locations: @@ -115,7 +137,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegio return region -def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> None: region = multiworld.get_region(name, player) if data.region_exits: for region_exit in data.region_exits: @@ -123,3 +145,34 @@ def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str exit_region = multiworld.get_region(region_exit, player) r_exit_stage.connect(exit_region) region.exits.append(r_exit_stage) + + +def create_classic_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld + menu = create_classic_region(multiworld, player, "Menu") + multiworld.regions.append(menu) + # By using a victory region, we can define it as being connected to by several regions + # which can then determine the availability of the victory. + victory_region = create_classic_region(multiworld, player, "Victory") + multiworld.regions.append(victory_region) + petrichor = create_classic_region(multiworld, player, "Petrichor V", + get_classic_item_pickups(ror2_options.total_locations.value)) + multiworld.regions.append(petrichor) + + # classic mode can get to victory from the beginning of the game + to_victory = Entrance(player, "beating game", petrichor) + petrichor.exits.append(to_victory) + to_victory.connect(victory_region) + + connection = Entrance(player, "Lobby", menu) + menu.exits.append(connection) + connection.connect(petrichor) + + +def create_classic_region(multiworld: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: + ret = Region(name, player, multiworld) + for location_name, location_id in locations.items(): + ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) + return ret diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py new file mode 100644 index 000000000000..d821763ef40c --- /dev/null +++ b/worlds/ror2/ror2environments.py @@ -0,0 +1,118 @@ +from typing import Dict, List, TypeVar + +# TODO probably move to Locations + +environment_vanilla_orderedstage_1_table: Dict[str, int] = { + "Distant Roost": 7, # blackbeach + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains": 15, # golemplains + "Titanic Plains (2)": 16, # golemplains2 +} +environment_vanilla_orderedstage_2_table: Dict[str, int] = { + "Abandoned Aqueduct": 17, # goolake + "Wetland Aspect": 12, # foggyswamp +} +environment_vanilla_orderedstage_3_table: Dict[str, int] = { + "Rallypoint Delta": 13, # frozenwall + "Scorched Acres": 47, # wispgraveyard +} +environment_vanilla_orderedstage_4_table: Dict[str, int] = { + "Abyssal Depths": 10, # dampcavesimple + "Siren's Call": 37, # shipgraveyard + "Sundered Grove": 35, # rootjungle +} +environment_vanilla_orderedstage_5_table: Dict[str, int] = { + "Sky Meadow": 38, # skymeadow +} + +environment_vanilla_hidden_realm_table: Dict[str, int] = { + "Hidden Realm: Bulwark's Ambry": 5, # artifactworld + "Hidden Realm: Bazaar Between Time": 6, # bazaar + "Hidden Realm: Gilded Coast": 14, # goldshores + "Hidden Realm: A Moment, Whole": 27, # limbo + "Hidden Realm: A Moment, Fractured": 33, # mysteryspace +} + +environment_vanilla_special_table: Dict[str, int] = { + "Void Fields": 4, # arena + "Commencement": 32, # moon2 +} + +environment_sotv_orderedstage_1_table: Dict[str, int] = { + "Siphoned Forest": 39, # snowyforest +} +environment_sotv_orderedstage_2_table: Dict[str, int] = { + "Aphelian Sanctuary": 3, # ancientloft +} +environment_sotv_orderedstage_3_table: Dict[str, int] = { + "Sulfur Pools": 41, # sulfurpools +} + +environment_sotv_special_table: Dict[str, int] = { + "Void Locus": 46, # voidstage + "The Planetarium": 45, # voidraid +} + +X = TypeVar("X") +Y = TypeVar("Y") + + +def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: + """Combine all dictionaries in a list together into one dictionary.""" + compressed: Dict[X, Y] = {} + for individual in list_of_dict: + compressed.update(individual) + return compressed + + +def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: + """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" + # find the length of the longest list + length = len(list_of_dict_1) + for list_of_dict_n in args: + length = max(length, len(list_of_dict_n)) + + # create a combined list with a length the same as the longest list + collapsed: List[Dict[X, Y]] = [{}] * length + # The reason the list_of_dict_1 is not directly used to make collapsed is + # side effects can occur if all the dictionaries are not manually unioned. + + # merge contents from list_of_dict_1 + for i in range(len(list_of_dict_1)): + collapsed[i] = {**collapsed[i], **list_of_dict_1[i]} + + # merge contents of remaining lists_of_dicts + for list_of_dict_n in args: + for i in range(len(list_of_dict_n)): + collapsed[i] = {**collapsed[i], **list_of_dict_n[i]} + + return collapsed + + +# TODO potentially these should only be created when they are directly referenced +# (unsure of the space/time cost of creating these initially) + +environment_vanilla_orderedstages_table = \ + [environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, + environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, + environment_vanilla_orderedstage_5_table] +environment_vanilla_table = \ + {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), + **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} + +environment_sotv_orderedstages_table = \ + [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, + environment_sotv_orderedstage_3_table] +environment_sotv_table = \ + {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} + +environment_non_orderedstages_table = \ + {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table} +environment_orderedstages_table = \ + collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) +environment_all_table = {**environment_vanilla_table, **environment_sotv_table} + + +def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: + """Shift all indexes in a dictionary by an offset""" + return {name: index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/Rules.py b/worlds/ror2/rules.py similarity index 60% rename from worlds/ror2/Rules.py rename to worlds/ror2/rules.py index 65c04d06cba6..442e6c0002aa 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/rules.py @@ -1,62 +1,71 @@ -from BaseClasses import MultiWorld, CollectionState from worlds.generic.Rules import set_rule, add_rule -from .Locations import orderedstage_location -from .RoR2Environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ - environment_orderedstages_table +from BaseClasses import MultiWorld +from .locations import get_locations +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from typing import Set, TYPE_CHECKING + +if TYPE_CHECKING: + from . import RiskOfRainWorld # Rule to see if it has access to the previous stage -def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int): +def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int) -> None: multiworld.get_entrance(entrance, player).access_rule = \ lambda state: state.has(entrance, player) and state.has(stage, player) +def has_all_items(multiworld: MultiWorld, items: Set[str], entrance: str, player: int) -> None: + multiworld.get_entrance(entrance, player).access_rule = \ + lambda state: state.has_all(items, player) and state.has(entrance, player) + + # Checks to see if chest/shrine are accessible -def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str): +def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ + -> None: if item_number == 1: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: state.has(environment, player) + # scavengers need to be locked till after a full loop since that is when they are capable of spawning. + # (While technically the requirement is just beating 5 stages, this will ensure that the player will have + # a long enough run to have enough director credits for scavengers and + # help prevent being stuck in the same stages until that point). if item_type == "Scavenger": multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage_4", player) + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: check_location(state, environment, player, item_number, item_type) -def check_location(state, environment: str, player: int, item_number: int, item_name: str): +def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) # unlock event to next set of stages -def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int): - if not multiworld.dlc_sotv[player]: - environment_name = multiworld.random.choices(list(environment_vanilla_orderedstages_table[stage_number].keys()), - k=1) - else: - environment_name = multiworld.random.choices(list(environment_orderedstages_table[stage_number].keys()), k=1) - multiworld.get_location(f"Stage_{stage_number + 1}", player).access_rule = \ - lambda state: get_one_of_the_stages(state, environment_name[0], player) - - -def get_one_of_the_stages(state: CollectionState, stage: str, player: int): - return state.has(stage, player) - - -def set_rules(multiworld: MultiWorld, player: int) -> None: - if multiworld.goal[player] == "classic": +def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int) -> None: + if stage_number == 4: + return + multiworld.get_entrance(f"OrderedStage_{stage_number + 1}", player).access_rule = \ + lambda state: state.has(f"Stage {stage_number + 1}", player) + + +def set_rules(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + multiworld = ror2_world.multiworld + ror2_options = ror2_world.options + if ror2_options.goal == "classic": # classic mode - total_locations = multiworld.total_locations[player].value # total locations for current player + total_locations = ror2_options.total_locations.value # total locations for current player else: # explore mode total_locations = len( - orderedstage_location.get_locations( - chests=multiworld.chests_per_stage[player].value, - shrines=multiworld.shrines_per_stage[player].value, - scavengers=multiworld.scavengers_per_stage[player].value, - scanners=multiworld.scanner_per_stage[player].value, - altars=multiworld.altars_per_stage[player].value, - dlc_sotv=multiworld.dlc_sotv[player].value + get_locations( + chests=ror2_options.chests_per_stage.value, + shrines=ror2_options.shrines_per_stage.value, + scavengers=ror2_options.scavengers_per_stage.value, + scanners=ror2_options.scanner_per_stage.value, + altars=ror2_options.altars_per_stage.value, + dlc_sotv=bool(ror2_options.dlc_sotv.value) ) ) @@ -64,14 +73,15 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: divisions = total_locations // event_location_step total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic - if multiworld.goal[player] == "classic": + if ror2_options.goal == "classic": # classic mode if divisions: for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25 if i * event_location_step != total_locations: event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player) set_rule(event_loc, - lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) + lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", + "Location", player)) # we want to create a rule for each of the 25 locations per division for n in range(i * event_location_step, (i + 1) * event_location_step + 1): if n > total_locations: @@ -84,27 +94,18 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player)) set_rule(multiworld.get_location("Victory", player), lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player)) - if total_revivals or multiworld.start_with_revive[player].value: + if total_revivals or ror2_options.start_with_revive.value: add_rule(multiworld.get_location("Victory", player), lambda state: state.has("Dio's Best Friend", player, - total_revivals + multiworld.start_with_revive[player])) + total_revivals + ror2_options.start_with_revive)) - elif multiworld.goal[player] == "explore": - # When explore_mode is used, - # scavengers need to be locked till after a full loop since that is when they are capable of spawning. - # (While technically the requirement is just beating 5 stages, this will ensure that the player will have - # a long enough run to have enough director credits for scavengers and - # help prevent being stuck in the same stages until that point.) - - for location in multiworld.get_locations(player): - if "Scavenger" in location.name: - add_rule(location, lambda state: state.has("Stage_5", player)) - # Regions - chests = multiworld.chests_per_stage[player] - shrines = multiworld.shrines_per_stage[player] - newts = multiworld.altars_per_stage[player] - scavengers = multiworld.scavengers_per_stage[player] - scanners = multiworld.scanner_per_stage[player] + else: + # explore mode + chests = ror2_options.chests_per_stage.value + shrines = ror2_options.shrines_per_stage.value + newts = ror2_options.altars_per_stage.value + scavengers = ror2_options.scavengers_per_stage.value + scanners = ror2_options.scanner_per_stage.value for i in range(len(environment_vanilla_orderedstages_table)): for environment_name, _ in environment_vanilla_orderedstages_table[i].items(): # Make sure to go through each location @@ -120,10 +121,10 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) get_stage_event(multiworld, player, i) - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: for i in range(len(environment_sotv_orderedstages_table)): for environment_name, _ in environment_sotv_orderedstages_table[i].items(): # Make sure to go through each location @@ -139,16 +140,19 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) - has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) - has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Bazaar Between Time", player) - has_entrance_access_rule(multiworld, f"Hidden Realm: Bazaar Between Time", "Void Fields", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Commencement", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Hidden Realm: A Moment, Fractured", player) + has_entrance_access_rule(multiworld, "Stage 1", "Hidden Realm: Bazaar Between Time", player) + has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player) + has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player) + has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player) has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player) - if multiworld.dlc_sotv[player]: - has_entrance_access_rule(multiworld, f"Stage_5", "Void Locus", player) - has_entrance_access_rule(multiworld, f"Void Locus", "The Planetarium", player) + if ror2_options.dlc_sotv: + has_entrance_access_rule(multiworld, "Stage 5", "The Planetarium", player) + has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player) + if ror2_options.victory == "voidling": + has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player) + # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/__init__.py b/worlds/ror2/test/__init__.py new file mode 100644 index 000000000000..87d8183ab847 --- /dev/null +++ b/worlds/ror2/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class RoR2TestBase(WorldTestBase): + game = "Risk of Rain 2" diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py new file mode 100644 index 000000000000..18d49944195d --- /dev/null +++ b/worlds/ror2/test/test_any_goal.py @@ -0,0 +1,26 @@ +from . import RoR2TestBase + + +class DLCTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "any" + } + + def test_commencement_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertBeatable(True) + + def test_planetarium_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertBeatable(True) + + def test_moment_whole_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_classic.py b/worlds/ror2/test/test_classic.py new file mode 100644 index 000000000000..90ed2302b272 --- /dev/null +++ b/worlds/ror2/test/test_classic.py @@ -0,0 +1,7 @@ +from . import RoR2TestBase + + +class ClassicTest(RoR2TestBase): + options = { + "goal": "classic", + } diff --git a/worlds/ror2/test/test_limbo_goal.py b/worlds/ror2/test/test_limbo_goal.py new file mode 100644 index 000000000000..f8757a917641 --- /dev/null +++ b/worlds/ror2/test/test_limbo_goal.py @@ -0,0 +1,15 @@ +from . import RoR2TestBase + + +class LimboGoalTest(RoR2TestBase): + options = { + "victory": "limbo" + } + + def test_limbo(self) -> None: + self.collect_all_but(["Hidden Realm: A Moment, Whole", "Victory"]) + self.assertFalse(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertTrue(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_mithrix_goal.py b/worlds/ror2/test/test_mithrix_goal.py new file mode 100644 index 000000000000..7ed9a2cd73a2 --- /dev/null +++ b/worlds/ror2/test/test_mithrix_goal.py @@ -0,0 +1,25 @@ +from . import RoR2TestBase + + +class MithrixGoalTest(RoR2TestBase): + options = { + "victory": "mithrix" + } + + def test_mithrix(self) -> None: + self.collect_all_but(["Commencement", "Victory"]) + self.assertFalse(self.can_reach_entrance("Commencement")) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_entrance("Commencement")) + self.assertBeatable(True) + + def test_stage5(self) -> None: + self.collect_all_but(["Stage 4", "Sky Meadow", "Victory"]) + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(False) + self.collect_by_name("Sky Meadow") + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.collect_by_name("Stage 4") + self.assertTrue(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_voidling_goal.py b/worlds/ror2/test/test_voidling_goal.py new file mode 100644 index 000000000000..a7520a5c5f95 --- /dev/null +++ b/worlds/ror2/test/test_voidling_goal.py @@ -0,0 +1,28 @@ +from . import RoR2TestBase + + +class VoidlingGoalTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "voidling" + } + + def test_planetarium(self) -> None: + self.collect_all_but(["The Planetarium", "Victory"]) + self.assertFalse(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertTrue(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(True) + + def test_void_locus_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Void Locus") + self.assertTrue(self.can_reach_entrance("Victory")) + + def test_commencement_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_location("Victory"))