RoR2: 1.20 content update ()

## 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:
kindasneaki 2023-02-05 13:51:03 -07:00 committed by GitHub
parent fb1a9e9c5a
commit cae1e683e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 908 additions and 116 deletions

View File

@ -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()}

View File

@ -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()}

View File

@ -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
}

126
worlds/ror2/Regions.py Normal file
View File

@ -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)

View File

@ -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()}

View File

@ -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)

View File

@ -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

View File

@ -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.