RoR2: 1.20 content update (#1396)
## Adding in Explore Mode: Features include: * Added in `environments` to be items. * `Location checks` are now `environment based` instead of being able to get them from anywhere. * Added in support for the `DLC Survivors of the void` which include `Void Items` and `3 new maps` that come with it. (option added to use DLC) --------- Co-authored-by: Dogpetkid <dogpetkid@gmail.com>
This commit is contained in:
parent
fb1a9e9c5a
commit
cae1e683e2
|
@ -1,13 +1,13 @@
|
|||
from typing import Dict
|
||||
from BaseClasses import Item
|
||||
from .Options import ItemWeights
|
||||
from .RoR2Environments import *
|
||||
|
||||
|
||||
class RiskOfRainItem(Item):
|
||||
game: str = "Risk of Rain 2"
|
||||
|
||||
|
||||
# 37000 - 38000
|
||||
# 37000 - 37699, 38000
|
||||
item_table: Dict[str, int] = {
|
||||
"Dio's Best Friend": 37001,
|
||||
"Common Item": 37002,
|
||||
|
@ -19,9 +19,24 @@ item_table: Dict[str, int] = {
|
|||
"Item Scrap, White": 37008,
|
||||
"Item Scrap, Green": 37009,
|
||||
"Item Scrap, Red": 37010,
|
||||
"Item Scrap, Yellow": 37011
|
||||
"Item Scrap, Yellow": 37011,
|
||||
"Void Item": 37012
|
||||
}
|
||||
|
||||
# 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,
|
||||
|
@ -32,6 +47,7 @@ default_weights: Dict[str, int] = {
|
|||
"Legendary Item": 8,
|
||||
"Boss Item": 4,
|
||||
"Lunar Item": 16,
|
||||
"Void Item": 16,
|
||||
"Equipment": 32
|
||||
}
|
||||
|
||||
|
@ -45,6 +61,7 @@ new_weights: Dict[str, int] = {
|
|||
"Legendary Item": 10,
|
||||
"Boss Item": 5,
|
||||
"Lunar Item": 10,
|
||||
"Void Item": 16,
|
||||
"Equipment": 20
|
||||
}
|
||||
|
||||
|
@ -58,6 +75,7 @@ uncommon_weights: Dict[str, int] = {
|
|||
"Legendary Item": 10,
|
||||
"Boss Item": 5,
|
||||
"Lunar Item": 15,
|
||||
"Void Item": 16,
|
||||
"Equipment": 20
|
||||
}
|
||||
|
||||
|
@ -71,6 +89,7 @@ legendary_weights: Dict[str, int] = {
|
|||
"Legendary Item": 100,
|
||||
"Boss Item": 5,
|
||||
"Lunar Item": 15,
|
||||
"Void Item": 16,
|
||||
"Equipment": 20
|
||||
}
|
||||
|
||||
|
@ -84,6 +103,7 @@ lunartic_weights: Dict[str, int] = {
|
|||
"Legendary Item": 0,
|
||||
"Boss Item": 0,
|
||||
"Lunar Item": 100,
|
||||
"Void Item": 0,
|
||||
"Equipment": 0
|
||||
}
|
||||
|
||||
|
@ -97,6 +117,7 @@ chaos_weights: Dict[str, int] = {
|
|||
"Legendary Item": 30,
|
||||
"Boss Item": 20,
|
||||
"Lunar Item": 60,
|
||||
"Void Item": 60,
|
||||
"Equipment": 40
|
||||
}
|
||||
|
||||
|
@ -110,6 +131,7 @@ no_scraps_weights: Dict[str, int] = {
|
|||
"Legendary Item": 15,
|
||||
"Boss Item": 5,
|
||||
"Lunar Item": 10,
|
||||
"Void Item": 16,
|
||||
"Equipment": 25
|
||||
}
|
||||
|
||||
|
@ -123,6 +145,7 @@ even_weights: Dict[str, int] = {
|
|||
"Legendary Item": 1,
|
||||
"Boss Item": 1,
|
||||
"Lunar Item": 1,
|
||||
"Void Item": 1,
|
||||
"Equipment": 1
|
||||
}
|
||||
|
||||
|
@ -136,6 +159,21 @@ scraps_only: Dict[str, int] = {
|
|||
"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
|
||||
}
|
||||
|
||||
|
@ -148,7 +186,8 @@ item_pool_weights: Dict[int, Dict[str, int]] = {
|
|||
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_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()}
|
||||
|
|
|
@ -1,13 +1,119 @@
|
|||
from typing import Dict
|
||||
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"
|
||||
|
||||
|
||||
# 37006 - 37506
|
||||
item_pickups: Dict[str, int] = {
|
||||
f"ItemPickup{i+1}": 37000+i for i in range(TotalLocations.range_end)
|
||||
}
|
||||
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()}
|
||||
|
|
|
@ -1,31 +1,99 @@
|
|||
from typing import Dict
|
||||
from Options import Option, DefaultOnToggle, Range, Choice
|
||||
from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Classic Mode: Every Item pickup increases fills a progress bar which gives location checks.
|
||||
|
||||
Explore Mode: Each environment will have location checks within each environment.
|
||||
environments will be locked in the item pool until received.
|
||||
"""
|
||||
display_name = "Game Mode"
|
||||
option_classic = 0
|
||||
option_explore = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class TotalLocations(Range):
|
||||
"""Number of location checks which are added to the Risk of Rain playthrough."""
|
||||
"""Classic Mode: Number of location checks which are added to the Risk of Rain playthrough."""
|
||||
display_name = "Total Locations"
|
||||
range_start = 10
|
||||
range_start = 40
|
||||
range_end = 250
|
||||
default = 20
|
||||
default = 40
|
||||
|
||||
|
||||
class ChestsPerEnvironment(Range):
|
||||
"""Explore Mode: The number of chest locations per environment."""
|
||||
display_name = "Chests per Environment"
|
||||
range_start = 2
|
||||
range_end = 20
|
||||
default = 10
|
||||
|
||||
|
||||
class ShrinesPerEnvironment(Range):
|
||||
"""Explore Mode: The number of shrine locations per environment."""
|
||||
display_name = "Shrines per Environment"
|
||||
range_start = 2
|
||||
range_end = 20
|
||||
default = 5
|
||||
|
||||
|
||||
class ScavengersPerEnvironment(Range):
|
||||
"""Explore Mode: The number of scavenger locations per environment."""
|
||||
display_name = "Scavenger per Environment"
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
default = 1
|
||||
|
||||
class ScannersPerEnvironment(Range):
|
||||
"""Explore Mode: The number of scanners locations per environment."""
|
||||
display_name = "Radio Scanners per Environment"
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
default = 1
|
||||
|
||||
class AltarsPerEnvironment(Range):
|
||||
"""Explore Mode: The number of altars locations per environment."""
|
||||
display_name = "Newts Per Environment"
|
||||
range_start = 0
|
||||
range_end = 2
|
||||
default = 1
|
||||
|
||||
class TotalRevivals(Range):
|
||||
"""Total Percentage of `Dio's Best Friend` item put in the item pool."""
|
||||
display_name = "Total Percentage Revivals Available"
|
||||
display_name = "Total Revives"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 4
|
||||
|
||||
|
||||
class ItemPickupStep(Range):
|
||||
"""Number of items to pick up before an AP Check is completed.
|
||||
"""
|
||||
Number of items to pick up before an AP Check is completed.
|
||||
Setting to 1 means every other pickup.
|
||||
Setting to 2 means every third pickup. So on..."""
|
||||
Setting to 2 means every third pickup. So on...
|
||||
"""
|
||||
display_name = "Item Pickup Step"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 2
|
||||
default = 1
|
||||
|
||||
class ShrineUseStep(Range):
|
||||
"""
|
||||
Explore Mode:
|
||||
Number of shrines to use up before an AP Check is completed.
|
||||
Setting to 1 means every other pickup.
|
||||
Setting to 2 means every third pickup. So on...
|
||||
"""
|
||||
display_name = "Shrine use Step"
|
||||
range_start = 0
|
||||
range_end = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class AllowLunarItems(DefaultOnToggle):
|
||||
|
@ -38,13 +106,33 @@ class StartWithRevive(DefaultOnToggle):
|
|||
display_name = "Start with a Revive"
|
||||
|
||||
|
||||
class FinalStageDeath(DefaultOnToggle):
|
||||
class FinalStageDeath(Toggle):
|
||||
"""Death on the final boss stage counts as a win."""
|
||||
display_name = "Final Stage Death is Win"
|
||||
|
||||
|
||||
class BeginWithLoop(Toggle):
|
||||
"""
|
||||
Enable to precollect a full loop of environments.
|
||||
Only has an effect with Explore Mode.
|
||||
"""
|
||||
display_name = "Begin With Loop"
|
||||
|
||||
|
||||
class DLC_SOTV(Toggle):
|
||||
"""
|
||||
Enable if you are using SOTV DLC.
|
||||
Affects environment availability for Explore Mode.
|
||||
Adds Void Items into the item pool
|
||||
"""
|
||||
display_name = "Enable DLC - SOTV"
|
||||
|
||||
|
||||
|
||||
class GreenScrap(Range):
|
||||
"""Weight of Green Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of Green Scraps in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Green Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -52,7 +140,9 @@ class GreenScrap(Range):
|
|||
|
||||
|
||||
class RedScrap(Range):
|
||||
"""Weight of Red Scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of Red Scraps in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Red Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -60,7 +150,9 @@ class RedScrap(Range):
|
|||
|
||||
|
||||
class YellowScrap(Range):
|
||||
"""Weight of yellow scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of yellow scraps in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Yellow Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -68,7 +160,9 @@ class YellowScrap(Range):
|
|||
|
||||
|
||||
class WhiteScrap(Range):
|
||||
"""Weight of white scraps in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of white scraps in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "White Scraps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -76,7 +170,9 @@ class WhiteScrap(Range):
|
|||
|
||||
|
||||
class CommonItem(Range):
|
||||
"""Weight of common items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of common items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Common Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -84,7 +180,9 @@ class CommonItem(Range):
|
|||
|
||||
|
||||
class UncommonItem(Range):
|
||||
"""Weight of uncommon items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of uncommon items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Uncommon Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -92,7 +190,9 @@ class UncommonItem(Range):
|
|||
|
||||
|
||||
class LegendaryItem(Range):
|
||||
"""Weight of legendary items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of legendary items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Legendary Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -100,7 +200,9 @@ class LegendaryItem(Range):
|
|||
|
||||
|
||||
class BossItem(Range):
|
||||
"""Weight of boss items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of boss items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Boss Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
@ -108,36 +210,54 @@ class BossItem(Range):
|
|||
|
||||
|
||||
class LunarItem(Range):
|
||||
"""Weight of lunar items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of lunar items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Lunar Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 16
|
||||
|
||||
|
||||
class VoidItem(Range):
|
||||
"""Weight of void items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')
|
||||
|
||||
(Ignored if Enable DLC - SOTV is 'No') """
|
||||
display_name = "Void Items"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 16
|
||||
|
||||
|
||||
class Equipment(Range):
|
||||
"""Weight of equipment items in the item pool. (Ignored unless Item Weight Presets is 'No')"""
|
||||
"""Weight of equipment items in the item pool.
|
||||
|
||||
(Ignored unless Item Weight Presets is 'No')"""
|
||||
display_name = "Equipment"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 32
|
||||
|
||||
|
||||
class ItemPoolPresetToggle(DefaultOnToggle):
|
||||
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"
|
||||
|
||||
|
||||
class ItemWeights(Choice):
|
||||
"""Preset choices for determining the weights of the item pool.
|
||||
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.
|
||||
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."""
|
||||
"""Set item_pool_presets to true if you want to use one of these presets.
|
||||
Preset choices for determining the weights of the item pool.
|
||||
- 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.
|
||||
- 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.
|
||||
- Void makes everything a void item."""
|
||||
display_name = "Item Weights"
|
||||
option_default = 0
|
||||
option_new = 1
|
||||
|
@ -148,6 +268,7 @@ class ItemWeights(Choice):
|
|||
option_no_scraps = 6
|
||||
option_even = 7
|
||||
option_scraps_only = 8
|
||||
option_void = 9
|
||||
|
||||
|
||||
# define a dictionary for the weights of the generated item pool.
|
||||
|
@ -161,17 +282,28 @@ ror2_weights: Dict[str, type(Option)] = {
|
|||
"legendary_item": LegendaryItem,
|
||||
"boss_item": BossItem,
|
||||
"lunar_item": LunarItem,
|
||||
"void_item": VoidItem,
|
||||
"equipment": Equipment
|
||||
}
|
||||
|
||||
ror2_options: Dict[str, type(Option)] = {
|
||||
"total_locations": TotalLocations,
|
||||
"total_revivals": TotalRevivals,
|
||||
"start_with_revive": StartWithRevive,
|
||||
"final_stage_death": FinalStageDeath,
|
||||
"item_pickup_step": ItemPickupStep,
|
||||
"enable_lunar": AllowLunarItems,
|
||||
"item_weights": ItemWeights,
|
||||
"item_pool_presets": ItemPoolPresetToggle,
|
||||
"goal": Goal,
|
||||
"total_locations": TotalLocations,
|
||||
"chests_per_stage": ChestsPerEnvironment,
|
||||
"shrines_per_stage": ShrinesPerEnvironment,
|
||||
"scavengers_per_stage": ScavengersPerEnvironment,
|
||||
"scanner_per_stage": ScannersPerEnvironment,
|
||||
"altars_per_stage": AltarsPerEnvironment,
|
||||
"total_revivals": TotalRevivals,
|
||||
"start_with_revive": StartWithRevive,
|
||||
"final_stage_death": FinalStageDeath,
|
||||
"begin_with_loop": BeginWithLoop,
|
||||
"dlc_sotv": DLC_SOTV,
|
||||
"death_link": DeathLink,
|
||||
"item_pickup_step": ItemPickupStep,
|
||||
"shrine_use_step": ShrineUseStep,
|
||||
"enable_lunar": AllowLunarItems,
|
||||
"item_weights": ItemWeights,
|
||||
"item_pool_presets": ItemPoolPresetToggle,
|
||||
**ror2_weights
|
||||
}
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
from typing import Dict, List, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import MultiWorld, Region, RegionType, Entrance
|
||||
from .Locations import location_table, RiskOfRainLocation
|
||||
|
||||
|
||||
class RoRRegionData(NamedTuple):
|
||||
locations: Optional[List[str]]
|
||||
region_exits: Optional[List[str]]
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int):
|
||||
# Default Locations
|
||||
non_dlc_regions: Dict[str, RoRRegionData] = {
|
||||
"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"]),
|
||||
"Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
|
||||
"Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]),
|
||||
"Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]),
|
||||
"Rallypoint Delta": RoRRegionData([], ["OrderedStage_3"]),
|
||||
"Scorched Acres": RoRRegionData([], ["OrderedStage_3"]),
|
||||
"Abyssal Depths": RoRRegionData([], ["OrderedStage_4"]),
|
||||
"Siren's Call": RoRRegionData([], ["OrderedStage_4"]),
|
||||
"Sundered Grove": RoRRegionData([], ["OrderedStage_4"]),
|
||||
"Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]),
|
||||
}
|
||||
# SOTV Regions
|
||||
dlc_regions: Dict[str, RoRRegionData] = {
|
||||
"Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]),
|
||||
"Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]),
|
||||
"Sulfur Pools": RoRRegionData([], ["OrderedStage_3"])
|
||||
}
|
||||
other_regions: Dict[str, RoRRegionData] = {
|
||||
"Commencement": RoRRegionData(None, ["Victory"]),
|
||||
"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"]),
|
||||
"OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]),
|
||||
"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"]),
|
||||
"Void Fields": RoRRegionData(None, []),
|
||||
"Victory": RoRRegionData(None, None),
|
||||
"Petrichor V": RoRRegionData(None, ["Victory"]),
|
||||
"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"]),
|
||||
"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])
|
||||
all_location_regions = {**non_dlc_regions}
|
||||
if multiworld.dlc_sotv[player]:
|
||||
all_location_regions = {**non_dlc_regions, **dlc_regions}
|
||||
|
||||
# Locations
|
||||
for key in all_location_regions:
|
||||
if key == "Menu":
|
||||
continue
|
||||
# Chests
|
||||
for i in range(0, chests):
|
||||
all_location_regions[key].locations.append(f"{key}: Chest {i + 1}")
|
||||
# Shrines
|
||||
for i in range(0, shrines):
|
||||
all_location_regions[key].locations.append(f"{key}: Shrine {i + 1}")
|
||||
# Scavengers
|
||||
if scavengers > 0:
|
||||
for i in range(0, scavengers):
|
||||
all_location_regions[key].locations.append(f"{key}: Scavenger {i + 1}")
|
||||
# Radio Scanners
|
||||
if scanners > 0:
|
||||
for i in range(0, scanners):
|
||||
all_location_regions[key].locations.append(f"{key}: Radio Scanner {i + 1}")
|
||||
# Newt Altars
|
||||
if newt > 0:
|
||||
for i in range(0, newt):
|
||||
all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}")
|
||||
regions_pool: Dict = {**all_location_regions, **other_regions}
|
||||
|
||||
# DLC Locations
|
||||
if multiworld.dlc_sotv[player]:
|
||||
non_dlc_regions["Menu"].region_exits.append("Siphoned Forest")
|
||||
other_regions["OrderedStage_2"].region_exits.append("Aphelian Sanctuary")
|
||||
other_regions["OrderedStage_3"].region_exits.append("Sulfur Pools")
|
||||
other_regions["Commencement"].region_exits.append("The Planetarium")
|
||||
other_regions["Void Fields"].region_exits.append("Void Locus")
|
||||
regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions}
|
||||
|
||||
# Create all the regions
|
||||
for name, data in regions_pool.items():
|
||||
multiworld.regions.append(create_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):
|
||||
region = Region(name, RegionType.Generic, name, player, multiworld)
|
||||
if data.locations:
|
||||
for location_name in data.locations:
|
||||
location_data = location_table.get(location_name)
|
||||
location = RiskOfRainLocation(player, location_name, location_data, region)
|
||||
region.locations.append(location)
|
||||
|
||||
return region
|
||||
|
||||
|
||||
def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData):
|
||||
region = multiworld.get_region(name, player)
|
||||
if data.region_exits:
|
||||
for region_exit in data.region_exits:
|
||||
r_exit_stage = Entrance(player, region_exit, region)
|
||||
exit_region = multiworld.get_region(region_exit, player)
|
||||
r_exit_stage.connect(exit_region)
|
||||
region.exits.append(r_exit_stage)
|
|
@ -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_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": 45, # voidstage
|
||||
"The Planetarium": 46, # 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()}
|
|
@ -1,33 +1,154 @@
|
|||
from BaseClasses import MultiWorld
|
||||
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
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int) -> None:
|
||||
total_locations = world.total_locations[player].value # total locations for current player
|
||||
# Rule to see if it has access to the previous stage
|
||||
def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int):
|
||||
multiworld.get_entrance(entrance, player).access_rule = \
|
||||
lambda state: state.has(entrance, player) and state.has(stage, 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):
|
||||
if item_number == 1:
|
||||
multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
|
||||
lambda state: state.has(environment, player)
|
||||
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)
|
||||
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):
|
||||
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":
|
||||
# classic mode
|
||||
total_locations = multiworld.total_locations[player].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
|
||||
)
|
||||
)
|
||||
|
||||
event_location_step = 25 # set an event location at these locations for "spheres"
|
||||
divisions = total_locations // event_location_step
|
||||
total_revivals = world.worlds[player].total_revivals # pulling this info we calculated in generate_basic
|
||||
total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic
|
||||
|
||||
if divisions:
|
||||
for i in range(1, divisions): # since divisions is the floor of total_locations / 25
|
||||
event_loc = world.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))
|
||||
for n in range(i * event_location_step, (i + 1) * event_location_step): # we want to create a rule for each of the 25 locations per division
|
||||
if n == i * event_location_step:
|
||||
set_rule(world.get_location(f"ItemPickup{n}", player),
|
||||
lambda state, event_item=event_loc.item.name: state.has(event_item, player))
|
||||
else:
|
||||
set_rule(world.get_location(f"ItemPickup{n}", player),
|
||||
lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player))
|
||||
for i in range(divisions * event_location_step, total_locations+1):
|
||||
set_rule(world.get_location(f"ItemPickup{i}", player),
|
||||
lambda state, i=i: state.can_reach(f"ItemPickup{i - 1}", "Location", player))
|
||||
set_rule(world.get_location("Victory", player),
|
||||
lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player))
|
||||
if total_revivals or world.start_with_revive[player].value:
|
||||
add_rule(world.get_location("Victory", player),
|
||||
lambda state: state.has("Dio's Best Friend", player,
|
||||
total_revivals + world.start_with_revive[player]))
|
||||
if multiworld.goal[player] == "classic":
|
||||
# classic mode
|
||||
if divisions:
|
||||
for i in range(1, divisions): # since divisions is the floor of total_locations / 25
|
||||
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))
|
||||
for n in range(i * event_location_step, (i + 1) * event_location_step): # we want to create a rule for each of the 25 locations per division
|
||||
if n == i * event_location_step:
|
||||
set_rule(multiworld.get_location(f"ItemPickup{n}", player),
|
||||
lambda state, event_item=event_loc.item.name: state.has(event_item, player))
|
||||
else:
|
||||
set_rule(multiworld.get_location(f"ItemPickup{n}", player),
|
||||
lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player))
|
||||
for i in range(divisions * event_location_step, total_locations+1):
|
||||
set_rule(multiworld.get_location(f"ItemPickup{i}", player),
|
||||
lambda state, i=i: state.can_reach(f"ItemPickup{i - 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:
|
||||
add_rule(multiworld.get_location("Victory", player),
|
||||
lambda state: state.has("Dio's Best Friend", player,
|
||||
total_revivals + multiworld.start_with_revive[player]))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
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():
|
||||
if location.player != player: continue # ignore all checks that don't belong to this 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]
|
||||
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
|
||||
if scavengers == 1:
|
||||
has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
|
||||
if scanners == 1:
|
||||
has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
|
||||
for chest in range(1, chests + 1):
|
||||
has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
|
||||
for shrine in range(1, shrines + 1):
|
||||
has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
|
||||
if newts > 0:
|
||||
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)
|
||||
get_stage_event(multiworld, player, i)
|
||||
|
||||
if multiworld.dlc_sotv[player]:
|
||||
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
|
||||
if scavengers == 1:
|
||||
has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger")
|
||||
if scanners == 1:
|
||||
has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner")
|
||||
for chest in range(1, chests + 1):
|
||||
has_location_access_rule(multiworld, environment_name, player, chest, "Chest")
|
||||
for shrine in range(1, shrines + 1):
|
||||
has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine")
|
||||
if newts > 0:
|
||||
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"Sky Meadow", "Hidden Realm: Bulwark's Ambry", player)
|
||||
has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player)
|
||||
has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Gilded Coast", 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)
|
||||
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)
|
||||
# Win Condition
|
||||
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import string
|
||||
from typing import Dict, List
|
||||
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, RegionType, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import RiskOfRainItem, item_pool_weights, item_table
|
||||
from .Locations import RiskOfRainLocation, item_pickups
|
||||
from .Options import ItemWeights, ror2_options
|
||||
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 *
|
||||
|
||||
client_version = 1
|
||||
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from .Options import ror2_options, ItemWeights
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Regions import create_regions
|
||||
|
||||
|
||||
class RiskOfWeb(WebWorld):
|
||||
|
@ -35,20 +35,58 @@ class RiskOfRainWorld(World):
|
|||
item_name_to_id = item_table
|
||||
location_name_to_id = item_pickups
|
||||
|
||||
data_version = 4
|
||||
data_version = 6
|
||||
required_client_version = (0, 3, 7)
|
||||
web = RiskOfWeb()
|
||||
total_revivals: int
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# figure out how many revivals should exist in the pool
|
||||
if self.multiworld.goal[self.player] == "classic":
|
||||
total_locations = self.multiworld.total_locations[self.player].value
|
||||
else:
|
||||
total_locations = len(
|
||||
orderedstage_location.get_locations(
|
||||
chests=self.multiworld.chests_per_stage[self.player].value,
|
||||
shrines=self.multiworld.shrines_per_stage[self.player].value,
|
||||
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
|
||||
scanners=self.multiworld.scanner_per_stage[self.player].value,
|
||||
altars=self.multiworld.altars_per_stage[self.player].value,
|
||||
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
|
||||
)
|
||||
)
|
||||
self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 *
|
||||
self.multiworld.total_locations[self.player].value)
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
|
||||
total_locations)
|
||||
# self.total_revivals = self.multiworld.total_revivals[self.player].value
|
||||
if self.multiworld.start_with_revive[self.player].value:
|
||||
self.total_revivals -= 1
|
||||
|
||||
def create_items(self) -> None:
|
||||
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
|
||||
if self.multiworld.start_with_revive[self.player]:
|
||||
self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player))
|
||||
|
||||
environments_pool = {}
|
||||
# only mess with the environments if they are set as items
|
||||
if self.multiworld.goal[self.player] == "explore":
|
||||
|
||||
# figure out all available ordered stages for each tier
|
||||
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
|
||||
if self.multiworld.dlc_sotv[self.player]:
|
||||
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)
|
||||
|
||||
if self.multiworld.dlc_sotv[self.player]:
|
||||
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
|
||||
environments_pool = {**environments_pool, **environment_offset_table}
|
||||
environments_to_precollect = 5 if self.multiworld.begin_with_loop[self.player].value 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)
|
||||
self.multiworld.push_precollected(self.create_item(unlock[0]))
|
||||
environments_pool.pop(unlock[0])
|
||||
|
||||
# if presets are enabled generate junk_pool from the selected preset
|
||||
pool_option = self.multiworld.item_weights[self.player].value
|
||||
junk_pool: Dict[str, int] = {}
|
||||
|
@ -70,61 +108,120 @@ class RiskOfRainWorld(World):
|
|||
"Legendary Item": self.multiworld.legendary_item[self.player].value,
|
||||
"Boss Item": self.multiworld.boss_item[self.player].value,
|
||||
"Lunar Item": self.multiworld.lunar_item[self.player].value,
|
||||
"Void Item": self.multiworld.void_item[self.player].value,
|
||||
"Equipment": self.multiworld.equipment[self.player].value
|
||||
}
|
||||
|
||||
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
|
||||
if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic):
|
||||
if not self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic:
|
||||
junk_pool.pop("Lunar Item")
|
||||
# remove void items from the pool
|
||||
if not self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void:
|
||||
junk_pool.pop("Void Item")
|
||||
|
||||
# Generate item pool
|
||||
itempool: List = []
|
||||
# Add revive items for the player
|
||||
itempool += ["Dio's Best Friend"] * self.total_revivals
|
||||
|
||||
for env_name, _ in environments_pool.items():
|
||||
itempool += [env_name]
|
||||
|
||||
# precollected environments are popped from the pool so counting like this is valid
|
||||
nonjunk_item_count = self.total_revivals + len(environments_pool)
|
||||
if self.multiworld.goal[self.player] == "classic":
|
||||
# classic mode
|
||||
total_locations = self.multiworld.total_locations[self.player].value
|
||||
else:
|
||||
# explore mode
|
||||
total_locations = len(
|
||||
orderedstage_location.get_locations(
|
||||
chests=self.multiworld.chests_per_stage[self.player].value,
|
||||
shrines=self.multiworld.shrines_per_stage[self.player].value,
|
||||
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
|
||||
scanners=self.multiworld.scanner_per_stage[self.player].value,
|
||||
altars=self.multiworld.altars_per_stage[self.player].value,
|
||||
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
|
||||
)
|
||||
)
|
||||
junk_item_count = total_locations - nonjunk_item_count
|
||||
# Fill remaining items with randomly generated junk
|
||||
itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
|
||||
k=self.multiworld.total_locations[self.player].value - self.total_revivals)
|
||||
k=junk_item_count)
|
||||
|
||||
# Convert itempool into real items
|
||||
itempool = list(map(lambda name: self.create_item(name), itempool))
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def set_rules(self) -> None:
|
||||
set_rules(self.multiworld, self.player)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu = create_region(self.multiworld, self.player, "Menu")
|
||||
petrichor = create_region(self.multiworld, self.player, "Petrichor V",
|
||||
[f"ItemPickup{i + 1}" for i in range(self.multiworld.total_locations[self.player].value)])
|
||||
|
||||
connection = Entrance(self.player, "Lobby", menu)
|
||||
menu.exits.append(connection)
|
||||
connection.connect(petrichor)
|
||||
if self.multiworld.goal[self.player] == "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.multiworld.total_locations[self.player].value))
|
||||
self.multiworld.regions.append(petrichor)
|
||||
|
||||
self.multiworld.regions += [menu, 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)
|
||||
|
||||
connection = Entrance(self.player, "Lobby", menu)
|
||||
menu.exits.append(connection)
|
||||
connection.connect(petrichor)
|
||||
else:
|
||||
# explore mode
|
||||
create_regions(self.multiworld, self.player)
|
||||
|
||||
create_events(self.multiworld, self.player)
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"itemPickupStep": self.multiworld.item_pickup_step[self.player].value,
|
||||
"shrineUseStep": self.multiworld.shrine_use_step[self.player].value,
|
||||
"goal": self.multiworld.goal[self.player].value,
|
||||
"seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)),
|
||||
"totalLocations": self.multiworld.total_locations[self.player].value,
|
||||
"chestsPerStage": self.multiworld.chests_per_stage[self.player].value,
|
||||
"shrinesPerStage": self.multiworld.shrines_per_stage[self.player].value,
|
||||
"scavengersPerStage": self.multiworld.scavengers_per_stage[self.player].value,
|
||||
"scannerPerStage": self.multiworld.scanner_per_stage[self.player].value,
|
||||
"altarsPerStage": self.multiworld.altars_per_stage[self.player].value,
|
||||
"totalRevivals": self.multiworld.total_revivals[self.player].value,
|
||||
"startWithDio": self.multiworld.start_with_revive[self.player].value,
|
||||
"FinalStageDeath": self.multiworld.final_stage_death[self.player].value
|
||||
"finalStageDeath": self.multiworld.final_stage_death[self.player].value,
|
||||
"deathLink": self.multiworld.death_link[self.player].value,
|
||||
}
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_id = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if name == "Dio's Best Friend":
|
||||
classification = ItemClassification.progression
|
||||
elif name in {"Equipment", "Legendary Item"}:
|
||||
elif name in {"Legendary Item", "Boss Item"}:
|
||||
classification = ItemClassification.useful
|
||||
else:
|
||||
classification = ItemClassification.filler
|
||||
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 comparions.
|
||||
|
||||
elif name in environment_ALL_table.keys():
|
||||
if name in {"Void Fields", "Hidden Realm: Bazaar Between Time", "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
|
||||
|
||||
|
@ -135,23 +232,31 @@ def create_events(world: MultiWorld, player: int) -> None:
|
|||
if total_locations / 25 == num_of_events:
|
||||
num_of_events -= 1
|
||||
world_region = world.get_region("Petrichor V", player)
|
||||
if world.goal[player] == "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.goal[player] == "explore":
|
||||
for n in range(1, 6):
|
||||
|
||||
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}", player))
|
||||
world_region.locations.append(event_loc)
|
||||
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))
|
||||
event_loc.show_in_spoiler = False
|
||||
event_region.locations.append(event_loc)
|
||||
|
||||
victory_event = RiskOfRainLocation(player, "Victory", None, world_region)
|
||||
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: List[str] = None) -> Region:
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region:
|
||||
ret = Region(name, RegionType.Generic, name, player, world)
|
||||
if locations:
|
||||
for location in locations:
|
||||
loc_id = item_pickups[location]
|
||||
location = RiskOfRainLocation(player, location, loc_id, ret)
|
||||
ret.locations.append(location)
|
||||
for location_name, location_id in locations.items():
|
||||
ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret))
|
||||
return ret
|
||||
|
|
|
@ -12,19 +12,34 @@ functionality in which certain chests (made clear via a location check progress
|
|||
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
|
||||
other players in other worlds.
|
||||
|
||||
There are two modes in risk of rain. Classic Mode and Explore Mode
|
||||
|
||||
Classic Mode:
|
||||
|
||||
- Classic mode implements pure multiworld
|
||||
functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
|
||||
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
|
||||
other players in other worlds.
|
||||
|
||||
Explore Mode:
|
||||
|
||||
- Just like in Classic mode chests will send out an item to the multiworld. The difference is that each environment
|
||||
will have a set amount that can be sent out and shrines along with other things that will need to be checked.
|
||||
Also, each environment is an item and, you'll need it to be able to access it.
|
||||
|
||||
## What is the goal of Risk of Rain 2 in Archipelago?
|
||||
|
||||
Just like in the original game, any way to "beat the game or obliterate" counts as a win. By default, if you die while
|
||||
on a final boss stage, that also counts as a win. (You can turn this off in your player settings.) **You do not need to
|
||||
complete all the location checks** to win; any item you don't collect is automatically sent out to the multiworld when
|
||||
you meet your goal.
|
||||
Just like in the original game, any way to "beat the game or obliterate" counts as a win. There is a setting that
|
||||
if you die while on a final boss stage, that also counts as a win.(You can turn this on in your player settings.)
|
||||
**You do not need to complete all the location checks** to win; any item you don't collect may be released if the
|
||||
server options allow.
|
||||
|
||||
If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you
|
||||
received from other players. Any items that you picked up the "normal" way will be lost.
|
||||
|
||||
Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in
|
||||
Simulacrum. So you could, for example, collect most of your items through a Simulacrum run, then finish a normal mode
|
||||
run while keeping the items you received via the multiworld.
|
||||
Simulacrum. So you could, for example, collect most of your items through a Simulacrum run(only works in classic mode),
|
||||
then finish a normal mode run while keeping the items you received via the multiworld.
|
||||
|
||||
## Can you play multiplayer?
|
||||
|
||||
|
@ -38,6 +53,8 @@ settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be
|
|||
for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op
|
||||
instance can't make progress towards multiple player slots in the multiworld.
|
||||
|
||||
Explore mode is untested in multiplayer and will likely not work until a later release.
|
||||
|
||||
## What Risk of Rain items can appear in other players' worlds?
|
||||
|
||||
The Risk of Rain items are:
|
||||
|
@ -49,6 +66,7 @@ The Risk of Rain items are:
|
|||
* `Lunar Item` (Blue items)
|
||||
* `Equipment` (Orange items)
|
||||
* `Dio's Best Friend` (Used if you set the YAML setting `total_revives_available` above `0`)
|
||||
* `Void Item` (Purple items) (needs dlc_sotv: enabled)
|
||||
|
||||
Each item grants you a random in-game item from the category it belongs to.
|
||||
|
||||
|
@ -57,6 +75,29 @@ in-game item of that tier will appear in the Risk of Rain player's inventory. If
|
|||
the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _
|
||||
the new equipment_ will take it's place. (If you want the old one back, pick it up.)
|
||||
|
||||
Explore Mode items are:
|
||||
|
||||
* `Titanic Plains (1)`, `Titanic Plains (2)`, `Distant Roost (1)`, `Distant Roost (2)`
|
||||
* `Abandoned Aqueduct`, `Wetland Aspect`
|
||||
* `Rallypoint Delta`, `Scorched Acres`
|
||||
* `Abyssal Depths`, `Siren's Call`, `Sundered Grove`
|
||||
* `Sky Meadow`
|
||||
* `Commencement`
|
||||
* `All the Hidden Realms`
|
||||
|
||||
Dlc_Sotv items
|
||||
* `Siphoned Forest`
|
||||
* `Aphelian Sanctuary`
|
||||
* `Sulfur Pools`
|
||||
* `Void Locus`
|
||||
|
||||
When a explore item is granted it will unlock that environment and will now be accessible to progress to victory! The
|
||||
game will still pick randomly which environment is next but it will first check to see if they are available. If you have
|
||||
them unlocked it will weight the game to have a ***higher chance*** to go to one you have checks versus one you have
|
||||
already completed. You will still not be able to goto a stage 3 environment from a stage 1 environment.
|
||||
|
||||
|
||||
|
||||
### How many items are there?
|
||||
|
||||
Since a Risk of Rain 2 run can go on indefinitely, you have to configure how many collectible items (also known as
|
||||
|
@ -65,6 +106,10 @@ to 250** items. The number of items will be randomized between all players, so y
|
|||
item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good
|
||||
ballpark if you want to have a similar number of items to most other games.)
|
||||
|
||||
In explore mode the amount of checks base on how many **chests, shrines, scavengers, radio scanners and, newt altars**
|
||||
are in the pool. With just the base game the numbers are **52 to 516** and with the dlc its **60 to 660** with
|
||||
everything on default being **216**
|
||||
|
||||
After you have completed the specified number of checks, you won't send anything else to the multiworld. You can
|
||||
receive up to the specified number of randomized items from the multiworld as the players find them. In either case,
|
||||
you can continue to collect items as normal in Risk of Rain 2 if you've already found all your location checks.
|
||||
|
|
Loading…
Reference in New Issue