RoR2: 1.3.0 content update (#2425)

This commit is contained in:
Rjosephson 2023-11-22 08:20:32 -07:00 committed by GitHub
parent 01b566b798
commit 79406faf27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 999 additions and 656 deletions

View File

@ -1,194 +0,0 @@
from BaseClasses import Item
from .Options import ItemWeights
from .RoR2Environments import *
class RiskOfRainItem(Item):
game: str = "Risk of Rain 2"
# 37000 - 37699, 38000
item_table: Dict[str, int] = {
"Dio's Best Friend": 37001,
"Common Item": 37002,
"Uncommon Item": 37003,
"Legendary Item": 37004,
"Boss Item": 37005,
"Lunar Item": 37006,
"Equipment": 37007,
"Item Scrap, White": 37008,
"Item Scrap, Green": 37009,
"Item Scrap, Red": 37010,
"Item Scrap, Yellow": 37011,
"Void Item": 37012,
"Beads of Fealty": 37013
}
# 37700 - 37699
##################################################
# environments
environment_offest = 37700
# add ALL environments into the item table
environment_offset_table = shift_by_offset(environment_ALL_table, environment_offest)
item_table.update(shift_by_offset(environment_ALL_table, environment_offest))
# use the sotv dlc in the item table so that all names can be looked up regardless of use
# end of environments
##################################################
default_weights: Dict[str, int] = {
"Item Scrap, Green": 16,
"Item Scrap, Red": 4,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 32,
"Common Item": 64,
"Uncommon Item": 32,
"Legendary Item": 8,
"Boss Item": 4,
"Lunar Item": 16,
"Void Item": 16,
"Equipment": 32
}
new_weights: Dict[str, int] = {
"Item Scrap, Green": 15,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 75,
"Uncommon Item": 40,
"Legendary Item": 10,
"Boss Item": 5,
"Lunar Item": 10,
"Void Item": 16,
"Equipment": 20
}
uncommon_weights: Dict[str, int] = {
"Item Scrap, Green": 45,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 45,
"Uncommon Item": 100,
"Legendary Item": 10,
"Boss Item": 5,
"Lunar Item": 15,
"Void Item": 16,
"Equipment": 20
}
legendary_weights: Dict[str, int] = {
"Item Scrap, Green": 15,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 50,
"Uncommon Item": 25,
"Legendary Item": 100,
"Boss Item": 5,
"Lunar Item": 15,
"Void Item": 16,
"Equipment": 20
}
lunartic_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Lunar Item": 100,
"Void Item": 0,
"Equipment": 0
}
chaos_weights: Dict[str, int] = {
"Item Scrap, Green": 80,
"Item Scrap, Red": 45,
"Item Scrap, Yellow": 30,
"Item Scrap, White": 100,
"Common Item": 100,
"Uncommon Item": 70,
"Legendary Item": 30,
"Boss Item": 20,
"Lunar Item": 60,
"Void Item": 60,
"Equipment": 40
}
no_scraps_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 100,
"Uncommon Item": 40,
"Legendary Item": 15,
"Boss Item": 5,
"Lunar Item": 10,
"Void Item": 16,
"Equipment": 25
}
even_weights: Dict[str, int] = {
"Item Scrap, Green": 1,
"Item Scrap, Red": 1,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 1,
"Common Item": 1,
"Uncommon Item": 1,
"Legendary Item": 1,
"Boss Item": 1,
"Lunar Item": 1,
"Void Item": 1,
"Equipment": 1
}
scraps_only: Dict[str, int] = {
"Item Scrap, Green": 70,
"Item Scrap, White": 100,
"Item Scrap, Red": 30,
"Item Scrap, Yellow": 5,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Lunar Item": 0,
"Void Item": 0,
"Equipment": 0
}
void_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Lunar Item": 0,
"Void Item": 100,
"Equipment": 0
}
item_pool_weights: Dict[int, Dict[str, int]] = {
ItemWeights.option_default: default_weights,
ItemWeights.option_new: new_weights,
ItemWeights.option_uncommon: uncommon_weights,
ItemWeights.option_legendary: legendary_weights,
ItemWeights.option_lunartic: lunartic_weights,
ItemWeights.option_chaos: chaos_weights,
ItemWeights.option_no_scraps: no_scraps_weights,
ItemWeights.option_even: even_weights,
ItemWeights.option_scraps_only: scraps_only,
ItemWeights.option_void: void_weights,
}
lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()}

View File

@ -1,119 +0,0 @@
from typing import Tuple
from BaseClasses import Location
from .Options import TotalLocations
from .Options import ChestsPerEnvironment
from .Options import ShrinesPerEnvironment
from .Options import ScavengersPerEnvironment
from .Options import ScannersPerEnvironment
from .Options import AltarsPerEnvironment
from .RoR2Environments import *
class RiskOfRainLocation(Location):
game: str = "Risk of Rain 2"
ror2_locations_start_id = 38000
def get_classic_item_pickups(n: int) -> Dict[str, int]:
"""Get n ItemPickups, capped at the max value for TotalLocations"""
n = max(n, 0)
n = min(n, TotalLocations.range_end)
return { f"ItemPickup{i+1}": ror2_locations_start_id+i for i in range(n) }
item_pickups = get_classic_item_pickups(TotalLocations.range_end)
location_table = item_pickups
def environment_abreviation(long_name:str) -> str:
"""convert long environment names to initials"""
abrev = ""
# go through every word finding a letter (or number) for an initial
for word in long_name.split():
initial = word[0]
for letter in word:
if letter.isalnum():
initial = letter
break
abrev+= initial
return abrev
# highest numbered orderedstages (this is so we can treat the easily caculate the check ids based on the environment and location "offset")
highest_orderedstage: int= max(compress_dict_list_horizontal(environment_orderedstages_table).values())
ror2_locations_start_orderedstage = ror2_locations_start_id + TotalLocations.range_end
class orderedstage_location:
"""A class to behave like a struct for storing the offsets of location types in the allocated space per orderedstage environments."""
# TODO is there a better, more generic way to do this?
offset_ChestsPerEnvironment = 0
offset_ShrinesPerEnvironment = offset_ChestsPerEnvironment + ChestsPerEnvironment.range_end
offset_ScavengersPerEnvironment = offset_ShrinesPerEnvironment + ShrinesPerEnvironment.range_end
offset_ScannersPerEnvironment = offset_ScavengersPerEnvironment + ScavengersPerEnvironment.range_end
offset_AltarsPerEnvironment = offset_ScannersPerEnvironment + ScannersPerEnvironment.range_end
# total space allocated to the locations in a single orderedstage environment
allocation = offset_AltarsPerEnvironment + AltarsPerEnvironment.range_end
def get_environment_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, environment: Tuple[str, int]) -> Dict[str, int]:
"""Get the locations within a specific environment"""
environment_name = environment[0]
environment_index = environment[1]
locations = {}
# due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers
# TODO perhaps a hashing algorithm could be used to compress this range and save "wasted" ids
environment_start_id = environment_index * orderedstage_location.allocation + ror2_locations_start_orderedstage
for n in range(chests):
locations.update({f"{environment_name}: Chest {n+1}": n + orderedstage_location.offset_ChestsPerEnvironment + environment_start_id})
for n in range(shrines):
locations.update({f"{environment_name}: Shrine {n+1}": n + orderedstage_location.offset_ShrinesPerEnvironment + environment_start_id})
for n in range(scavengers):
locations.update({f"{environment_name}: Scavenger {n+1}": n + orderedstage_location.offset_ScavengersPerEnvironment + environment_start_id})
for n in range(scanners):
locations.update({f"{environment_name}: Radio Scanner {n+1}": n + orderedstage_location.offset_ScannersPerEnvironment + environment_start_id})
for n in range(altars):
locations.update({f"{environment_name}: Newt Altar {n+1}": n + orderedstage_location.offset_AltarsPerEnvironment + environment_start_id})
return locations
def get_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, dlc_sotv:bool) -> Dict[str, int]:
"""Get a dictionary of locations for the ordedstage environments with the locations from the parameters."""
locations = {}
orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table)
if(dlc_sotv): orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table))
# for every environment, generate the respective locations
for environment_name, environment_index in orderedstages.items():
# locations = locations | orderedstage_location.get_environment_locations(
locations.update(orderedstage_location.get_environment_locations(
chests=chests,
shrines=shrines,
scavengers=scavengers,
scanners=scanners,
altars=altars,
environment=(environment_name, environment_index)
))
return locations
def getall_locations(dlc_sotv:bool=True) -> Dict[str, int]:
"""
Get all locations in ordered stages.
Set dlc_sotv to true for the SOTV DLC to be included.
"""
# to get all locations, attempt using as many locations as possible
return orderedstage_location.get_locations(
chests=ChestsPerEnvironment.range_end,
shrines=ShrinesPerEnvironment.range_end,
scavengers=ScavengersPerEnvironment.range_end,
scanners=ScannersPerEnvironment.range_end,
altars=AltarsPerEnvironment.range_end,
dlc_sotv=dlc_sotv
)
ror2_location_post_orderedstage = ror2_locations_start_orderedstage + highest_orderedstage*orderedstage_location.allocation
location_table.update(orderedstage_location.getall_locations())
# use the sotv dlc in the lookup table so that all ids can be looked up regardless of use
lookup_id_to_name: Dict[int, str] = {id: name for name, id in location_table.items()}

View File

@ -1,118 +0,0 @@
from typing import Dict, List, TypeVar
# TODO probably move to Locations
environment_vanilla_orderedstage_1_table: Dict[str, int] = {
"Distant Roost": 7, # blackbeach
"Distant Roost (2)": 8, # blackbeach2
"Titanic Plains": 15, # golemplains
"Titanic Plains (2)": 16, # golemplains2
}
environment_vanilla_orderedstage_2_table: Dict[str, int] = {
"Abandoned Aqueduct": 17, # goolake
"Wetland Aspect": 12, # foggyswamp
}
environment_vanilla_orderedstage_3_table: Dict[str, int] = {
"Rallypoint Delta": 13, # frozenwall
"Scorched Acres": 47, # wispgraveyard
}
environment_vanilla_orderedstage_4_table: Dict[str, int] = {
"Abyssal Depths": 10, # dampcavesimple
"Siren's Call": 37, # shipgraveyard
"Sundered Grove": 35, # rootjungle
}
environment_vanilla_orderedstage_5_table: Dict[str, int] = {
"Sky Meadow": 38, # skymeadow
}
environment_vanilla_hidden_realm_table: Dict[str, int] = {
"Hidden Realm: Bulwark's Ambry": 5, # artifactworld
"Hidden Realm: Bazaar Between Time": 6, # bazaar
"Hidden Realm: Gilded Coast": 14, # goldshores
"Hidden Realm: A Moment, Whole": 27, # limbo
"Hidden Realm: A Moment, Fractured": 33, # mysteryspace
}
environment_vanilla_special_table: Dict[str, int] = {
"Void Fields": 4, # arena
"Commencement": 32, # moon2
}
environment_sotv_orderedstage_1_table: Dict[str, int] = {
"Siphoned Forest": 39, # snowyforest
}
environment_sotv_orderedstage_2_table: Dict[str, int] = {
"Aphelian Sanctuary": 3, # ancientloft
}
environment_sotv_orderedstage_3_table: Dict[str, int] = {
"Sulfur Pools": 41, # sulfurpools
}
environment_sotv_orderedstage_4_table: Dict[str, int] = { }
environment_sotv_orderedstage_5_table: Dict[str, int] = { }
# TODO idk much and idc much about simulacrum, is there a forced order or something?
environment_sotv_simulacrum_table: Dict[str, int] = {
"The Simulacrum (Aphelian Sanctuary)": 20, # itancientloft
"The Simulacrum (Abyssal Depths)": 21, # itdampcave
"The Simulacrum (Rallypoint Delta)": 22, # itfrozenwall
"The Simulacrum (Titanic Plains)": 23, # itgolemplains
"The Simulacrum (Abandoned Aqueduct)": 24, # itgoolake
"The Simulacrum (Commencement)": 25, # itmoon
"The Simulacrum (Sky Meadow)": 26, # itskymeadow
}
environment_sotv_special_table: Dict[str, int] = {
"Void Locus": 46, # voidstage
"The Planetarium": 45, # voidraid
}
X = TypeVar("X")
Y = TypeVar("Y")
def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]:
"""Combine all dictionaries in a list together into one dictionary."""
compressed: Dict[X,Y] = {}
for individual in list_of_dict: compressed.update(individual)
return compressed
def collapse_dict_list_vertical(list_of_dict1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]:
"""Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length."""
# find the length of the longest list
length = len(list_of_dict1)
for list_of_dictN in args:
length = max(length, len(list_of_dictN))
# create a combined list with a length the same as the longest list
collapsed = [{}] * (length)
# The reason the list_of_dict1 is not directly used to make collapsed is
# side effects can occur if all the dictionaries are not manually unioned.
# merge contents from list_of_dict1
for i in range(len(list_of_dict1)):
collapsed[i] = {**collapsed[i], **list_of_dict1[i]}
# merge contents of remaining lists_of_dicts
for list_of_dictN in args:
for i in range(len(list_of_dictN)):
collapsed[i] = {**collapsed[i], **list_of_dictN[i]}
return collapsed
# TODO potentially these should only be created when they are directly referenced (unsure of the space/time cost of creating these initially)
environment_vanilla_orderedstages_table = [ environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, environment_vanilla_orderedstage_5_table ]
environment_vanilla_table = {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table}
environment_sotv_orderedstages_table = [ environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table, environment_sotv_orderedstage_4_table, environment_sotv_orderedstage_5_table ]
environment_sotv_non_simulacrum_table = {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table}
environment_sotv_table = {**environment_sotv_non_simulacrum_table}
environment_non_orderedstages_table = {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_simulacrum_table, **environment_sotv_special_table}
environment_orderedstages_table = collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table)
environment_ALL_table = {**environment_vanilla_table, **environment_sotv_table}
def shift_by_offset(dictionary: Dict[str, int], offset:int) -> Dict[str, int]:
"""Shift all indexes in a dictionary by an offset"""
return {name:index+offset for name, index in dictionary.items()}

View File

@ -1,14 +1,16 @@
import string
from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest
from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location
from .Rules import set_rules
from .RoR2Environments import *
from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler_table, environment_offset
from .locations import RiskOfRainLocation, item_pickups, get_locations
from .rules import set_rules
from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \
environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset
from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from .Options import ItemWeights, ROR2Options
from BaseClasses import Item, ItemClassification, Tutorial
from .options import ItemWeights, ROR2Options
from worlds.AutoWorld import World, WebWorld
from .Regions import create_regions
from .regions import create_explore_regions, create_classic_regions
from typing import List, Dict, Any
class RiskOfWeb(WebWorld):
@ -18,7 +20,7 @@ class RiskOfWeb(WebWorld):
"English",
"setup_en.md",
"setup/en",
["Ijwu"]
["Ijwu", "Kindasneaki"]
)]
@ -32,38 +34,53 @@ class RiskOfRainWorld(World):
options_dataclass = ROR2Options
options: ROR2Options
topology_present = False
item_name_to_id = item_table
item_name_to_id = {name: data.code for name, data in item_table.items()}
item_name_groups = {
"Stages": {name for name, data in item_table.items() if data.category == "Stage"},
"Environments": {name for name, data in item_table.items() if data.category == "Environment"},
"Upgrades": {name for name, data in item_table.items() if data.category == "Upgrade"},
"Fillers": {name for name, data in item_table.items() if data.category == "Filler"},
"Traps": {name for name, data in item_table.items() if data.category == "Trap"},
}
location_name_to_id = item_pickups
data_version = 7
required_client_version = (0, 4, 2)
data_version = 8
required_client_version = (0, 4, 4)
web = RiskOfWeb()
total_revivals: int
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
self.junk_pool: Dict[str, int] = {}
def generate_early(self) -> None:
# figure out how many revivals should exist in the pool
if self.options.goal == "classic":
total_locations = self.options.total_locations.value
else:
total_locations = len(
orderedstage_location.get_locations(
get_locations(
chests=self.options.chests_per_stage.value,
shrines=self.options.shrines_per_stage.value,
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
dlc_sotv=self.options.dlc_sotv.value
dlc_sotv=bool(self.options.dlc_sotv.value)
)
)
self.total_revivals = int(self.options.total_revivals.value / 100 *
total_locations)
if self.options.start_with_revive:
self.total_revivals -= 1
if self.options.victory == "voidling" and not self.options.dlc_sotv:
self.options.victory.value = self.options.victory.option_any
def create_regions(self) -> None:
if self.options.goal == "classic":
# classic mode
create_classic_regions(self)
else:
# explore mode
create_explore_regions(self)
self.create_events()
def create_items(self) -> None:
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
@ -77,25 +94,26 @@ class RiskOfRainWorld(World):
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
if self.options.dlc_sotv:
environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table)
environment_available_orderedstages_table = \
collapse_dict_list_vertical(environment_available_orderedstages_table,
environment_sotv_orderedstages_table)
environments_pool = shift_by_offset(environment_vanilla_table, environment_offest)
environments_pool = shift_by_offset(environment_vanilla_table, environment_offset)
if self.options.dlc_sotv:
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset)
environments_pool = {**environments_pool, **environment_offset_table}
environments_to_precollect = 5 if self.options.begin_with_loop else 1
# percollect environments for each stage (or just stage 1)
for i in range(environments_to_precollect):
unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
self.multiworld.push_precollected(self.create_item(unlock[0]))
environments_pool.pop(unlock[0])
# Generate item pool
itempool: List = []
itempool: List[str] = ["Beads of Fealty", "Radar Scanner"]
# Add revive items for the player
itempool += ["Dio's Best Friend"] * self.total_revivals
itempool += ["Beads of Fealty"]
for env_name, _ in environments_pool.items():
itempool += [env_name]
@ -105,38 +123,28 @@ class RiskOfRainWorld(World):
total_locations = self.options.total_locations.value
else:
# explore mode
# Add Stage items for logic gates
itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"]
total_locations = len(
orderedstage_location.get_locations(
get_locations(
chests=self.options.chests_per_stage.value,
shrines=self.options.shrines_per_stage.value,
scavengers=self.options.scavengers_per_stage.value,
scanners=self.options.scanner_per_stage.value,
altars=self.options.altars_per_stage.value,
dlc_sotv=self.options.dlc_sotv.value
dlc_sotv=bool(self.options.dlc_sotv.value)
)
)
# Create junk items
self.junk_pool = self.create_junk_pool()
junk_pool = self.create_junk_pool()
# Fill remaining items with randomly generated junk
while len(itempool) < total_locations:
itempool.append(self.get_filler_item_name())
filler = self.random.choices(*zip(*junk_pool.items()), k=total_locations - len(itempool))
itempool.extend(filler)
# Convert itempool into real items
itempool = list(map(lambda name: self.create_item(name), itempool))
self.multiworld.itempool += itempool
self.multiworld.itempool += map(self.create_item, itempool)
def set_rules(self) -> None:
set_rules(self.multiworld, self.player)
def get_filler_item_name(self) -> str:
if not self.junk_pool:
self.junk_pool = self.create_junk_pool()
weights = [data for data in self.junk_pool.values()]
filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights,
k=1)[0]
return filler
def create_junk_pool(self) -> Dict:
def create_junk_pool(self) -> Dict[str, int]:
# if presets are enabled generate junk_pool from the selected preset
pool_option = self.options.item_weights.value
junk_pool: Dict[str, int] = {}
@ -144,7 +152,7 @@ class RiskOfRainWorld(World):
# generate chaos weights if the preset is chosen
if pool_option == ItemWeights.option_chaos:
for name, max_value in item_pool_weights[pool_option].items():
junk_pool[name] = self.multiworld.random.randint(0, max_value)
junk_pool[name] = self.random.randint(0, max_value)
else:
junk_pool = item_pool_weights[pool_option].copy()
else: # generate junk pool from user created presets
@ -159,10 +167,22 @@ class RiskOfRainWorld(World):
"Boss Item": self.options.boss_item.value,
"Lunar Item": self.options.lunar_item.value,
"Void Item": self.options.void_item.value,
"Equipment": self.options.equipment.value
"Equipment": self.options.equipment.value,
"Money": self.options.money.value,
"Lunar Coin": self.options.lunar_coin.value,
"1000 Exp": self.options.experience.value,
"Mountain Trap": self.options.mountain_trap.value,
"Time Warp Trap": self.options.time_warp_trap.value,
"Combat Trap": self.options.combat_trap.value,
"Teleport Trap": self.options.teleport_trap.value,
}
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
# remove trap items from the pool (excluding lunar items)
if not self.options.enable_trap:
junk_pool.pop("Mountain Trap")
junk_pool.pop("Time Warp Trap")
junk_pool.pop("Combat Trap")
junk_pool.pop("Teleport Trap")
# remove lunar items from the pool
if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic):
junk_pool.pop("Lunar Item")
# remove void items from the pool
@ -171,98 +191,58 @@ class RiskOfRainWorld(World):
return junk_pool
def create_regions(self) -> None:
def create_item(self, name: str) -> Item:
data = item_table[name]
return RiskOfRainItem(name, data.item_type, data.code, self.player)
if self.options.goal == "classic":
# classic mode
menu = create_region(self.multiworld, self.player, "Menu")
self.multiworld.regions.append(menu)
# By using a victory region, we can define it as being connected to by several regions
# which can then determine the availability of the victory.
victory_region = create_region(self.multiworld, self.player, "Victory")
self.multiworld.regions.append(victory_region)
petrichor = create_region(self.multiworld, self.player, "Petrichor V",
get_classic_item_pickups(self.options.total_locations.value))
self.multiworld.regions.append(petrichor)
def set_rules(self) -> None:
set_rules(self)
# classic mode can get to victory from the beginning of the game
to_victory = Entrance(self.player, "beating game", petrichor)
petrichor.exits.append(to_victory)
to_victory.connect(victory_region)
def get_filler_item_name(self) -> str:
weights = [data.weight for data in filler_table.values()]
filler = self.multiworld.random.choices([filler for filler in filler_table.keys()], weights,
k=1)[0]
return filler
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):
options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations",
"chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
"scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive",
"final_stage_death", "death_link", casing="camel")
def fill_slot_data(self) -> Dict[str, Any]:
options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations",
"chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
"scanner_per_stage", "altars_per_stage", "total_revivals",
"start_with_revive", "final_stage_death", "death_link",
casing="camel")
return {
**options_dict,
"seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)),
"seed": "".join(self.random.choice(string.digits) for _ in range(16)),
"offset": offset
}
def create_item(self, name: str) -> Item:
item_id = item_table[name]
classification = ItemClassification.filler
if name in {"Dio's Best Friend", "Beads of Fealty"}:
classification = ItemClassification.progression
elif name in {"Legendary Item", "Boss Item"}:
classification = ItemClassification.useful
elif name == "Lunar Item":
classification = ItemClassification.trap
# Only check for an item to be a environment unlock if those are known to be in the pool.
# This should shave down comparisons.
elif name in environment_ALL_table.keys():
if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}:
classification = ItemClassification.useful
else:
classification = ItemClassification.progression
item = RiskOfRainItem(name, classification, item_id, self.player)
return item
def create_events(world: MultiWorld, player: int) -> None:
total_locations = world.worlds[player].options.total_locations.value
num_of_events = total_locations // 25
if total_locations / 25 == num_of_events:
num_of_events -= 1
world_region = world.get_region("Petrichor V", player)
if world.worlds[player].options.goal == "classic":
# only setup Pickups when using classic_mode
for i in range(num_of_events):
event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region)
event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player))
event_loc.access_rule = \
lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player)
world_region.locations.append(event_loc)
elif world.worlds[player].options.goal == "explore":
for n in range(1, 6):
event_region = world.get_region(f"OrderedStage_{n}", player)
event_loc = RiskOfRainLocation(player, f"Stage_{n}", None, event_region)
event_loc.place_locked_item(RiskOfRainItem(f"Stage_{n}", ItemClassification.progression, None, player))
def create_events(self) -> None:
total_locations = self.options.total_locations.value
num_of_events = total_locations // 25
if total_locations / 25 == num_of_events:
num_of_events -= 1
world_region = self.multiworld.get_region("Petrichor V", self.player)
if self.options.goal == "classic":
# classic mode
# only setup Pickups when using classic_mode
for i in range(num_of_events):
event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region)
event_loc.place_locked_item(
RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None,
self.player))
event_loc.access_rule = \
lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", self.player)
world_region.locations.append(event_loc)
else:
# explore mode
event_region = self.multiworld.get_region("OrderedStage_5", self.player)
event_loc = RiskOfRainLocation(self.player, "Stage 5", None, event_region)
event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player))
event_loc.show_in_spoiler = False
event_region.locations.append(event_loc)
event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player)
victory_region = world.get_region("Victory", player)
victory_event = RiskOfRainLocation(player, "Victory", None, victory_region)
victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player))
world_region.locations.append(victory_event)
def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region:
ret = Region(name, player, world)
for location_name, location_id in locations.items():
ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret))
return ret
victory_region = self.multiworld.get_region("Victory", self.player)
victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region)
victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player))
victory_region.locations.append(victory_event)

View File

@ -55,4 +55,15 @@ the player's YAML.
You can talk to other in the multiworld chat using the RoR2 chat. All other multiworld
remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can
also optionally connect to the multiworld using the text client, which can be found in the
[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases).
[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases).
### In-Game Commands
These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following:
- `archipelago_connect <url> <port> <slot> [password]` example: "archipelago_connect archipelago.gg 38281 SlotName".
- `archipelago_deathlink true/false` Toggle deathlink.
- `archipelago_disconnect` Disconnect from AP.
- `archipelago_final_stage_death true/false` Toggle final stage death.
Explore Mode only
- `archipelago_show_unlocked_stages` Show which stages have been received.
- `archipelago_highlight_satellite true/false` This will highlight the satellite to make it easier to see (Default false).

309
worlds/ror2/items.py Normal file
View File

@ -0,0 +1,309 @@
from BaseClasses import Item, ItemClassification
from .options import ItemWeights
from .ror2environments import environment_all_table
from typing import NamedTuple, Optional, Dict
class RiskOfRainItem(Item):
game: str = "Risk of Rain 2"
class RiskOfRainItemData(NamedTuple):
category: str
code: int
item_type: ItemClassification = ItemClassification.filler
weight: Optional[int] = None
offset: int = 37000
filler_offset: int = offset + 300
trap_offset: int = offset + 400
stage_offset: int = offset + 500
environment_offset: int = offset + 700
# Upgrade item ids 37002 - 37012
upgrade_table: Dict[str, RiskOfRainItemData] = {
"Common Item": RiskOfRainItemData("Upgrade", 2 + offset, ItemClassification.filler, 64),
"Uncommon Item": RiskOfRainItemData("Upgrade", 3 + offset, ItemClassification.filler, 32),
"Legendary Item": RiskOfRainItemData("Upgrade", 4 + offset, ItemClassification.useful, 8),
"Boss Item": RiskOfRainItemData("Upgrade", 5 + offset, ItemClassification.useful, 4),
"Equipment": RiskOfRainItemData("Upgrade", 7 + offset, ItemClassification.filler, 32),
"Item Scrap, White": RiskOfRainItemData("Upgrade", 8 + offset, ItemClassification.filler, 32),
"Item Scrap, Green": RiskOfRainItemData("Upgrade", 9 + offset, ItemClassification.filler, 16),
"Item Scrap, Red": RiskOfRainItemData("Upgrade", 10 + offset, ItemClassification.filler, 4),
"Item Scrap, Yellow": RiskOfRainItemData("Upgrade", 11 + offset, ItemClassification.filler, 1),
"Void Item": RiskOfRainItemData("Upgrade", 12 + offset, ItemClassification.filler, 16),
}
# Other item ids 37001, 37013-37014
other_table: Dict[str, RiskOfRainItemData] = {
"Dio's Best Friend": RiskOfRainItemData("ExtraLife", 1 + offset, ItemClassification.progression_skip_balancing),
"Beads of Fealty": RiskOfRainItemData("Beads", 13 + offset, ItemClassification.progression),
"Radar Scanner": RiskOfRainItemData("Radar", 14 + offset, ItemClassification.useful),
}
# Filler item ids 37301 - 37303
filler_table: Dict[str, RiskOfRainItemData] = {
"Money": RiskOfRainItemData("Filler", 1 + filler_offset, ItemClassification.filler, 64),
"Lunar Coin": RiskOfRainItemData("Filler", 2 + filler_offset, ItemClassification.filler, 20),
"1000 Exp": RiskOfRainItemData("Filler", 3 + filler_offset, ItemClassification.filler, 40),
}
# Trap item ids 37401 - 37404 (Lunar items used to be part of the upgrade item list, so keeping the id the same)
trap_table: Dict[str, RiskOfRainItemData] = {
"Lunar Item": RiskOfRainItemData("Trap", 6 + offset, ItemClassification.trap, 16),
"Mountain Trap": RiskOfRainItemData("Trap", 1 + trap_offset, ItemClassification.trap, 5),
"Time Warp Trap": RiskOfRainItemData("Trap", 2 + trap_offset, ItemClassification.trap, 20),
"Combat Trap": RiskOfRainItemData("Trap", 3 + trap_offset, ItemClassification.trap, 20),
"Teleport Trap": RiskOfRainItemData("Trap", 4 + trap_offset, ItemClassification.trap, 10),
}
# Stage item ids 37501 - 37504
stage_table: Dict[str, RiskOfRainItemData] = {
"Stage 1": RiskOfRainItemData("Stage", 1 + stage_offset, ItemClassification.progression),
"Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression),
"Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression),
"Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression),
}
item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table}
# Environment item ids 37700 - 37746
##################################################
# environments
# add ALL environments into the item table
def create_environment_table(name: str, environment_id: int, environment_classification: ItemClassification) \
-> Dict[str, RiskOfRainItemData]:
return {name: RiskOfRainItemData("Environment", environment_offset + environment_id, environment_classification)}
environment_table: Dict[str, RiskOfRainItemData] = {}
# use the sotv dlc in the item table so that all names can be looked up regardless of use
for data, key in environment_all_table.items():
classification = ItemClassification.progression
if data in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast"}:
classification = ItemClassification.useful
environment_table.update(create_environment_table(data, key, classification))
item_table.update(environment_table)
# end of environments
##################################################
default_weights: Dict[str, int] = {
"Item Scrap, Green": 16,
"Item Scrap, Red": 4,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 32,
"Common Item": 64,
"Uncommon Item": 32,
"Legendary Item": 8,
"Boss Item": 4,
"Void Item": 16,
"Equipment": 32,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
new_weights: Dict[str, int] = {
"Item Scrap, Green": 15,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 75,
"Uncommon Item": 40,
"Legendary Item": 10,
"Boss Item": 5,
"Void Item": 16,
"Equipment": 20,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
uncommon_weights: Dict[str, int] = {
"Item Scrap, Green": 45,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 45,
"Uncommon Item": 100,
"Legendary Item": 10,
"Boss Item": 5,
"Void Item": 16,
"Equipment": 20,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
legendary_weights: Dict[str, int] = {
"Item Scrap, Green": 15,
"Item Scrap, Red": 5,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 30,
"Common Item": 50,
"Uncommon Item": 25,
"Legendary Item": 100,
"Boss Item": 5,
"Void Item": 16,
"Equipment": 20,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
chaos_weights: Dict[str, int] = {
"Item Scrap, Green": 80,
"Item Scrap, Red": 45,
"Item Scrap, Yellow": 30,
"Item Scrap, White": 100,
"Common Item": 100,
"Uncommon Item": 70,
"Legendary Item": 30,
"Boss Item": 20,
"Void Item": 60,
"Equipment": 40,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
no_scraps_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 100,
"Uncommon Item": 40,
"Legendary Item": 15,
"Boss Item": 5,
"Void Item": 16,
"Equipment": 25,
"Money": 64,
"Lunar Coin": 20,
"1000 Exp": 40,
"Lunar Item": 10,
"Mountain Trap": 4,
"Time Warp Trap": 20,
"Combat Trap": 20,
"Teleport Trap": 20
}
even_weights: Dict[str, int] = {
"Item Scrap, Green": 1,
"Item Scrap, Red": 1,
"Item Scrap, Yellow": 1,
"Item Scrap, White": 1,
"Common Item": 1,
"Uncommon Item": 1,
"Legendary Item": 1,
"Boss Item": 1,
"Void Item": 1,
"Equipment": 1,
"Money": 1,
"Lunar Coin": 1,
"1000 Exp": 1,
"Lunar Item": 1,
"Mountain Trap": 1,
"Time Warp Trap": 1,
"Combat Trap": 1,
"Teleport Trap": 1
}
scraps_only: Dict[str, int] = {
"Item Scrap, Green": 70,
"Item Scrap, White": 100,
"Item Scrap, Red": 30,
"Item Scrap, Yellow": 5,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Void Item": 0,
"Equipment": 0,
"Money": 20,
"Lunar Coin": 10,
"1000 Exp": 10,
"Lunar Item": 0,
"Mountain Trap": 5,
"Time Warp Trap": 10,
"Combat Trap": 10,
"Teleport Trap": 10
}
lunartic_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Void Item": 0,
"Equipment": 0,
"Money": 20,
"Lunar Coin": 10,
"1000 Exp": 10,
"Lunar Item": 100,
"Mountain Trap": 5,
"Time Warp Trap": 10,
"Combat Trap": 10,
"Teleport Trap": 10
}
void_weights: Dict[str, int] = {
"Item Scrap, Green": 0,
"Item Scrap, Red": 0,
"Item Scrap, Yellow": 0,
"Item Scrap, White": 0,
"Common Item": 0,
"Uncommon Item": 0,
"Legendary Item": 0,
"Boss Item": 0,
"Void Item": 100,
"Equipment": 0,
"Money": 20,
"Lunar Coin": 10,
"1000 Exp": 10,
"Lunar Item": 0,
"Mountain Trap": 5,
"Time Warp Trap": 10,
"Combat Trap": 10,
"Teleport Trap": 10
}
item_pool_weights: Dict[int, Dict[str, int]] = {
ItemWeights.option_default: default_weights,
ItemWeights.option_new: new_weights,
ItemWeights.option_uncommon: uncommon_weights,
ItemWeights.option_legendary: legendary_weights,
ItemWeights.option_chaos: chaos_weights,
ItemWeights.option_no_scraps: no_scraps_weights,
ItemWeights.option_even: even_weights,
ItemWeights.option_scraps_only: scraps_only,
ItemWeights.option_lunartic: lunartic_weights,
ItemWeights.option_void: void_weights,
}

89
worlds/ror2/locations.py Normal file
View File

@ -0,0 +1,89 @@
from typing import Dict
from BaseClasses import Location
from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \
ScannersPerEnvironment, AltarsPerEnvironment
from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \
environment_sotv_orderedstages_table
class RiskOfRainLocation(Location):
game: str = "Risk of Rain 2"
ror2_locations_start_id = 38000
def get_classic_item_pickups(n: int) -> Dict[str, int]:
"""Get n ItemPickups, capped at the max value for TotalLocations"""
n = max(n, 0)
n = min(n, TotalLocations.range_end)
return {f"ItemPickup{i + 1}": ror2_locations_start_id + i for i in range(n)}
item_pickups = get_classic_item_pickups(TotalLocations.range_end)
location_table = item_pickups
# this is so we can easily calculate the environment and location "offset" ids
ror2_locations_start_ordered_stage = ror2_locations_start_id + TotalLocations.range_end
# TODO is there a better, more generic way to do this?
offset_chests = 0
offset_shrines = offset_chests + ChestsPerEnvironment.range_end
offset_scavengers = offset_shrines + ShrinesPerEnvironment.range_end
offset_scanners = offset_scavengers + ScavengersPerEnvironment.range_end
offset_altars = offset_scanners + ScannersPerEnvironment.range_end
# total space allocated to the locations in a single orderedstage environment
allocation = offset_altars + AltarsPerEnvironment.range_end
def get_environment_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int,
environment_name: str, environment_index: int) -> Dict[str, int]:
"""Get the locations within a specific environment"""
locations = {}
# due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers
environment_start_id = environment_index * allocation + ror2_locations_start_ordered_stage
for n in range(chests):
locations.update({f"{environment_name}: Chest {n + 1}": n + offset_chests + environment_start_id})
for n in range(shrines):
locations.update({f"{environment_name}: Shrine {n + 1}": n + offset_shrines + environment_start_id})
for n in range(scavengers):
locations.update({f"{environment_name}: Scavenger {n + 1}": n + offset_scavengers + environment_start_id})
for n in range(scanners):
locations.update({f"{environment_name}: Radio Scanner {n + 1}": n + offset_scanners + environment_start_id})
for n in range(altars):
locations.update({f"{environment_name}: Newt Altar {n + 1}": n + offset_altars + environment_start_id})
return locations
def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \
-> Dict[str, int]:
"""Get a dictionary of locations for the orderedstage environments with the locations from the parameters."""
locations = {}
orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table)
if dlc_sotv:
orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table))
# for every environment, generate the respective locations
for environment_name, environment_index in orderedstages.items():
locations.update(get_environment_locations(
chests=chests,
shrines=shrines,
scavengers=scavengers,
scanners=scanners,
altars=altars,
environment_name=environment_name,
environment_index=environment_index),
)
return locations
# Get all locations in ordered stages.
location_table.update(get_locations(
chests=ChestsPerEnvironment.range_end,
shrines=ShrinesPerEnvironment.range_end,
scavengers=ScavengersPerEnvironment.range_end,
scanners=ScannersPerEnvironment.range_end,
altars=AltarsPerEnvironment.range_end,
dlc_sotv=True,
))

View File

@ -4,7 +4,7 @@ from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCo
# NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks
# Be careful when changing the range_end values not to go into another game's IDs
# NOTE that these changes to range_end must also be reflected in the RoR2 client so it understands the same ids.
# NOTE that these changes to range_end must also be reflected in the RoR2 client, so it understands the same ids.
class Goal(Choice):
"""
@ -19,6 +19,21 @@ class Goal(Choice):
default = 1
class Victory(Choice):
"""
Mithrix: Defeat Mithrix in Commencement
Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.)
Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole
Any: Any victory in the game will count. See Final Stage Death for additional ways.
"""
display_name = "Victory Condition"
option_any = 0
option_mithrix = 1
option_voidling = 2
option_limbo = 3
default = 0
class TotalLocations(Range):
"""Classic Mode: Number of location checks which are added to the Risk of Rain playthrough."""
display_name = "Total Locations"
@ -100,6 +115,11 @@ class ShrineUseStep(Range):
default = 0
class AllowTrapItems(Toggle):
"""Allows Trap items in the item pool."""
display_name = "Enable Trap Items"
class AllowLunarItems(DefaultOnToggle):
"""Allows Lunar items in the item pool."""
display_name = "Enable Lunar Item Shuffling"
@ -111,10 +131,14 @@ class StartWithRevive(DefaultOnToggle):
class FinalStageDeath(Toggle):
"""The following will count as a win if set to true:
"""The following will count as a win if set to "true", and victory is set to "any":
Dying in Commencement.
Dying in The Planetarium.
Obliterating yourself"""
Obliterating yourself
If not use the following to tell if final stage death will count:
Victory: mithrix - only dying in Commencement will count.
Victory: voidling - only dying in The Planetarium will count.
Victory: limbo - Obliterating yourself will count."""
display_name = "Final Stage Death is Win"
@ -247,6 +271,76 @@ class Equipment(Range):
default = 32
class Money(Range):
"""Weight of money items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Money"
range_start = 0
range_end = 100
default = 64
class LunarCoin(Range):
"""Weight of lunar coin items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Lunar Coins"
range_start = 0
range_end = 100
default = 20
class Experience(Range):
"""Weight of 1000 exp items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "1000 Exp"
range_start = 0
range_end = 100
default = 40
class MountainTrap(Range):
"""Weight of mountain trap items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Mountain Trap"
range_start = 0
range_end = 100
default = 5
class TimeWarpTrap(Range):
"""Weight of time warp trap items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Time Warp Trap"
range_start = 0
range_end = 100
default = 20
class CombatTrap(Range):
"""Weight of combat trap items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Combat Trap"
range_start = 0
range_end = 100
default = 20
class TeleportTrap(Range):
"""Weight of teleport trap items in the item pool.
(Ignored unless Item Weight Presets is 'No')"""
display_name = "Teleport Trap"
range_start = 0
range_end = 100
default = 20
class ItemPoolPresetToggle(Toggle):
"""Will use the item weight presets when set to true, otherwise will use the custom set item pool weights."""
display_name = "Use Item Weight Presets"
@ -258,28 +352,30 @@ class ItemWeights(Choice):
- New is a test for a potential adjustment to the default weights.
- Uncommon puts a large number of uncommon items in the pool.
- Legendary puts a large number of legendary items in the pool.
- Lunartic makes everything a lunar item.
- Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
- Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being
too easy.
- No Scraps removes all scrap items from the item pool.
- Even generates the item pool with every item having an even weight.
- Scraps Only will be only scrap items in the item pool.
- Lunartic makes everything a lunar item.
- Void makes everything a void item."""
display_name = "Item Weights"
option_default = 0
option_new = 1
option_uncommon = 2
option_legendary = 3
option_lunartic = 4
option_chaos = 5
option_no_scraps = 6
option_even = 7
option_scraps_only = 8
option_chaos = 4
option_no_scraps = 5
option_even = 6
option_scraps_only = 7
option_lunartic = 8
option_void = 9
@dataclass
class ROR2Options(PerGameCommonOptions):
goal: Goal
victory: Victory
total_locations: TotalLocations
chests_per_stage: ChestsPerEnvironment
shrines_per_stage: ShrinesPerEnvironment
@ -294,6 +390,7 @@ class ROR2Options(PerGameCommonOptions):
death_link: DeathLink
item_pickup_step: ItemPickupStep
shrine_use_step: ShrineUseStep
enable_trap: AllowTrapItems
enable_lunar: AllowLunarItems
item_weights: ItemWeights
item_pool_presets: ItemPoolPresetToggle
@ -309,3 +406,10 @@ class ROR2Options(PerGameCommonOptions):
lunar_item: LunarItem
void_item: VoidItem
equipment: Equipment
money: Money
lunar_coin: LunarCoin
experience: Experience
mountain_trap: MountainTrap
time_warp_trap: TimeWarpTrap
combat_trap: CombatTrap
teleport_trap: TeleportTrap

View File

@ -1,7 +1,10 @@
from typing import Dict, List, NamedTuple, Optional
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
from BaseClasses import MultiWorld, Region, Entrance
from .Locations import location_table, RiskOfRainLocation
from BaseClasses import Region, Entrance, MultiWorld
from .locations import location_table, RiskOfRainLocation, get_classic_item_pickups
if TYPE_CHECKING:
from . import RiskOfRainWorld
class RoRRegionData(NamedTuple):
@ -9,10 +12,14 @@ class RoRRegionData(NamedTuple):
region_exits: Optional[List[str]]
def create_regions(multiworld: MultiWorld, player: int):
def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
player = ror2_world.player
ror2_options = ror2_world.options
multiworld = ror2_world.multiworld
# Default Locations
non_dlc_regions: Dict[str, RoRRegionData] = {
"Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", "Titanic Plains", "Titanic Plains (2)"]),
"Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)",
"Titanic Plains", "Titanic Plains (2)"]),
"Distant Roost": RoRRegionData([], ["OrderedStage_1"]),
"Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
"Titanic Plains": RoRRegionData([], ["OrderedStage_1"]),
@ -34,33 +41,36 @@ def create_regions(multiworld: MultiWorld, player: int):
}
other_regions: Dict[str, RoRRegionData] = {
"Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]),
"OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", "Commencement"]),
"OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured",
"Commencement"]),
"OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time",
"Hidden Realm: Gilded Coast", "Abandoned Aqueduct", "Wetland Aspect"]),
"Hidden Realm: Gilded Coast", "Abandoned Aqueduct",
"Wetland Aspect"]),
"OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]),
"OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", "Sundered Grove"]),
"OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call",
"Sundered Grove"]),
"OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]),
"Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]),
"Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory"]),
"Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory", "Petrichor V"]),
"Void Fields": RoRRegionData(None, []),
"Victory": RoRRegionData(None, None),
"Petrichor V": RoRRegionData(None, ["Victory"]),
"Petrichor V": RoRRegionData(None, []),
"Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None),
"Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]),
"Hidden Realm: Gilded Coast": RoRRegionData(None, None)
}
dlc_other_regions: Dict[str, RoRRegionData] = {
"The Planetarium": RoRRegionData(None, ["Victory"]),
"The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]),
"Void Locus": RoRRegionData(None, ["The Planetarium"])
}
# Totals of each item
chests = int(multiworld.chests_per_stage[player])
shrines = int(multiworld.shrines_per_stage[player])
scavengers = int(multiworld.scavengers_per_stage[player])
scanners = int(multiworld.scanner_per_stage[player])
newt = int(multiworld.altars_per_stage[player])
chests = int(ror2_options.chests_per_stage)
shrines = int(ror2_options.shrines_per_stage)
scavengers = int(ror2_options.scavengers_per_stage)
scanners = int(ror2_options.scanner_per_stage)
newt = int(ror2_options.altars_per_stage)
all_location_regions = {**non_dlc_regions}
if multiworld.dlc_sotv[player]:
if ror2_options.dlc_sotv:
all_location_regions = {**non_dlc_regions, **dlc_regions}
# Locations
@ -88,23 +98,35 @@ def create_regions(multiworld: MultiWorld, player: int):
regions_pool: Dict = {**all_location_regions, **other_regions}
# DLC Locations
if multiworld.dlc_sotv[player]:
if ror2_options.dlc_sotv:
non_dlc_regions["Menu"].region_exits.append("Siphoned Forest")
other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary")
other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools")
other_regions["Void Fields"].region_exits.append("Void Locus")
other_regions["Commencement"].region_exits.append("The Planetarium")
regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions}
# Check to see if Victory needs to be removed from regions
if ror2_options.victory == "mithrix":
other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
dlc_other_regions["The Planetarium"].region_exits.pop(0)
elif ror2_options.victory == "voidling":
other_regions["Commencement"].region_exits.pop(0)
other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0)
elif ror2_options.victory == "limbo":
other_regions["Commencement"].region_exits.pop(0)
dlc_other_regions["The Planetarium"].region_exits.pop(0)
# Create all the regions
for name, data in regions_pool.items():
multiworld.regions.append(create_region(multiworld, player, name, data))
multiworld.regions.append(create_explore_region(multiworld, player, name, data))
# Connect all the regions to their exits
for name, data in regions_pool.items():
create_connections_in_regions(multiworld, player, name, data)
def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData):
def create_explore_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> Region:
region = Region(name, player, multiworld)
if data.locations:
for location_name in data.locations:
@ -115,7 +137,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegio
return region
def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData):
def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> None:
region = multiworld.get_region(name, player)
if data.region_exits:
for region_exit in data.region_exits:
@ -123,3 +145,34 @@ def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str
exit_region = multiworld.get_region(region_exit, player)
r_exit_stage.connect(exit_region)
region.exits.append(r_exit_stage)
def create_classic_regions(ror2_world: "RiskOfRainWorld") -> None:
player = ror2_world.player
ror2_options = ror2_world.options
multiworld = ror2_world.multiworld
menu = create_classic_region(multiworld, player, "Menu")
multiworld.regions.append(menu)
# By using a victory region, we can define it as being connected to by several regions
# which can then determine the availability of the victory.
victory_region = create_classic_region(multiworld, player, "Victory")
multiworld.regions.append(victory_region)
petrichor = create_classic_region(multiworld, player, "Petrichor V",
get_classic_item_pickups(ror2_options.total_locations.value))
multiworld.regions.append(petrichor)
# classic mode can get to victory from the beginning of the game
to_victory = Entrance(player, "beating game", petrichor)
petrichor.exits.append(to_victory)
to_victory.connect(victory_region)
connection = Entrance(player, "Lobby", menu)
menu.exits.append(connection)
connection.connect(petrichor)
def create_classic_region(multiworld: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region:
ret = Region(name, player, multiworld)
for location_name, location_id in locations.items():
ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret))
return ret

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_special_table: Dict[str, int] = {
"Void Locus": 46, # voidstage
"The Planetarium": 45, # voidraid
}
X = TypeVar("X")
Y = TypeVar("Y")
def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]:
"""Combine all dictionaries in a list together into one dictionary."""
compressed: Dict[X, Y] = {}
for individual in list_of_dict:
compressed.update(individual)
return compressed
def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]:
"""Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length."""
# find the length of the longest list
length = len(list_of_dict_1)
for list_of_dict_n in args:
length = max(length, len(list_of_dict_n))
# create a combined list with a length the same as the longest list
collapsed: List[Dict[X, Y]] = [{}] * length
# The reason the list_of_dict_1 is not directly used to make collapsed is
# side effects can occur if all the dictionaries are not manually unioned.
# merge contents from list_of_dict_1
for i in range(len(list_of_dict_1)):
collapsed[i] = {**collapsed[i], **list_of_dict_1[i]}
# merge contents of remaining lists_of_dicts
for list_of_dict_n in args:
for i in range(len(list_of_dict_n)):
collapsed[i] = {**collapsed[i], **list_of_dict_n[i]}
return collapsed
# TODO potentially these should only be created when they are directly referenced
# (unsure of the space/time cost of creating these initially)
environment_vanilla_orderedstages_table = \
[environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table,
environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table,
environment_vanilla_orderedstage_5_table]
environment_vanilla_table = \
{**compress_dict_list_horizontal(environment_vanilla_orderedstages_table),
**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table}
environment_sotv_orderedstages_table = \
[environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table,
environment_sotv_orderedstage_3_table]
environment_sotv_table = \
{**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table}
environment_non_orderedstages_table = \
{**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table}
environment_orderedstages_table = \
collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table)
environment_all_table = {**environment_vanilla_table, **environment_sotv_table}
def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]:
"""Shift all indexes in a dictionary by an offset"""
return {name: index+offset for name, index in dictionary.items()}

View File

@ -1,62 +1,71 @@
from BaseClasses import MultiWorld, CollectionState
from worlds.generic.Rules import set_rule, add_rule
from .Locations import orderedstage_location
from .RoR2Environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \
environment_orderedstages_table
from BaseClasses import MultiWorld
from .locations import get_locations
from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table
from typing import Set, TYPE_CHECKING
if TYPE_CHECKING:
from . import RiskOfRainWorld
# Rule to see if it has access to the previous stage
def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int):
def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int) -> None:
multiworld.get_entrance(entrance, player).access_rule = \
lambda state: state.has(entrance, player) and state.has(stage, player)
def has_all_items(multiworld: MultiWorld, items: Set[str], entrance: str, player: int) -> None:
multiworld.get_entrance(entrance, player).access_rule = \
lambda state: state.has_all(items, player) and state.has(entrance, player)
# Checks to see if chest/shrine are accessible
def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str):
def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\
-> None:
if item_number == 1:
multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
lambda state: state.has(environment, player)
# scavengers need to be locked till after a full loop since that is when they are capable of spawning.
# (While technically the requirement is just beating 5 stages, this will ensure that the player will have
# a long enough run to have enough director credits for scavengers and
# help prevent being stuck in the same stages until that point).
if item_type == "Scavenger":
multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
lambda state: state.has(environment, player) and state.has("Stage_4", player)
lambda state: state.has(environment, player) and state.has("Stage 5", player)
else:
multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \
lambda state: check_location(state, environment, player, item_number, item_type)
def check_location(state, environment: str, player: int, item_number: int, item_name: str):
def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool:
return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player)
# unlock event to next set of stages
def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int):
if not multiworld.dlc_sotv[player]:
environment_name = multiworld.random.choices(list(environment_vanilla_orderedstages_table[stage_number].keys()),
k=1)
else:
environment_name = multiworld.random.choices(list(environment_orderedstages_table[stage_number].keys()), k=1)
multiworld.get_location(f"Stage_{stage_number + 1}", player).access_rule = \
lambda state: get_one_of_the_stages(state, environment_name[0], player)
def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int) -> None:
if stage_number == 4:
return
multiworld.get_entrance(f"OrderedStage_{stage_number + 1}", player).access_rule = \
lambda state: state.has(f"Stage {stage_number + 1}", player)
def get_one_of_the_stages(state: CollectionState, stage: str, player: int):
return state.has(stage, player)
def set_rules(multiworld: MultiWorld, player: int) -> None:
if multiworld.goal[player] == "classic":
def set_rules(ror2_world: "RiskOfRainWorld") -> None:
player = ror2_world.player
multiworld = ror2_world.multiworld
ror2_options = ror2_world.options
if ror2_options.goal == "classic":
# classic mode
total_locations = multiworld.total_locations[player].value # total locations for current player
total_locations = ror2_options.total_locations.value # total locations for current player
else:
# explore mode
total_locations = len(
orderedstage_location.get_locations(
chests=multiworld.chests_per_stage[player].value,
shrines=multiworld.shrines_per_stage[player].value,
scavengers=multiworld.scavengers_per_stage[player].value,
scanners=multiworld.scanner_per_stage[player].value,
altars=multiworld.altars_per_stage[player].value,
dlc_sotv=multiworld.dlc_sotv[player].value
get_locations(
chests=ror2_options.chests_per_stage.value,
shrines=ror2_options.shrines_per_stage.value,
scavengers=ror2_options.scavengers_per_stage.value,
scanners=ror2_options.scanner_per_stage.value,
altars=ror2_options.altars_per_stage.value,
dlc_sotv=bool(ror2_options.dlc_sotv.value)
)
)
@ -64,14 +73,15 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
divisions = total_locations // event_location_step
total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic
if multiworld.goal[player] == "classic":
if ror2_options.goal == "classic":
# classic mode
if divisions:
for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25
if i * event_location_step != total_locations:
event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player)
set_rule(event_loc,
lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player))
lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}",
"Location", player))
# we want to create a rule for each of the 25 locations per division
for n in range(i * event_location_step, (i + 1) * event_location_step + 1):
if n > total_locations:
@ -84,27 +94,18 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player))
set_rule(multiworld.get_location("Victory", player),
lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player))
if total_revivals or multiworld.start_with_revive[player].value:
if total_revivals or ror2_options.start_with_revive.value:
add_rule(multiworld.get_location("Victory", player),
lambda state: state.has("Dio's Best Friend", player,
total_revivals + multiworld.start_with_revive[player]))
total_revivals + ror2_options.start_with_revive))
elif multiworld.goal[player] == "explore":
# When explore_mode is used,
# scavengers need to be locked till after a full loop since that is when they are capable of spawning.
# (While technically the requirement is just beating 5 stages, this will ensure that the player will have
# a long enough run to have enough director credits for scavengers and
# help prevent being stuck in the same stages until that point.)
for location in multiworld.get_locations(player):
if "Scavenger" in location.name:
add_rule(location, lambda state: state.has("Stage_5", player))
# Regions
chests = multiworld.chests_per_stage[player]
shrines = multiworld.shrines_per_stage[player]
newts = multiworld.altars_per_stage[player]
scavengers = multiworld.scavengers_per_stage[player]
scanners = multiworld.scanner_per_stage[player]
else:
# explore mode
chests = ror2_options.chests_per_stage.value
shrines = ror2_options.shrines_per_stage.value
newts = ror2_options.altars_per_stage.value
scavengers = ror2_options.scavengers_per_stage.value
scanners = ror2_options.scanner_per_stage.value
for i in range(len(environment_vanilla_orderedstages_table)):
for environment_name, _ in environment_vanilla_orderedstages_table[i].items():
# Make sure to go through each location
@ -120,10 +121,10 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
for newt in range(1, newts + 1):
has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
if i > 0:
has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player)
has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player)
get_stage_event(multiworld, player, i)
if multiworld.dlc_sotv[player]:
if ror2_options.dlc_sotv:
for i in range(len(environment_sotv_orderedstages_table)):
for environment_name, _ in environment_sotv_orderedstages_table[i].items():
# Make sure to go through each location
@ -139,16 +140,19 @@ def set_rules(multiworld: MultiWorld, player: int) -> None:
for newt in range(1, newts + 1):
has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
if i > 0:
has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player)
has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player)
has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
player)
has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Bazaar Between Time", player)
has_entrance_access_rule(multiworld, f"Hidden Realm: Bazaar Between Time", "Void Fields", player)
has_entrance_access_rule(multiworld, f"Stage_5", "Commencement", player)
has_entrance_access_rule(multiworld, f"Stage_5", "Hidden Realm: A Moment, Fractured", player)
has_entrance_access_rule(multiworld, "Stage 1", "Hidden Realm: Bazaar Between Time", player)
has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player)
has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player)
has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player)
has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player)
if multiworld.dlc_sotv[player]:
has_entrance_access_rule(multiworld, f"Stage_5", "Void Locus", player)
has_entrance_access_rule(multiworld, f"Void Locus", "The Planetarium", player)
if ror2_options.dlc_sotv:
has_entrance_access_rule(multiworld, "Stage 5", "The Planetarium", player)
has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player)
if ror2_options.victory == "voidling":
has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player)
# Win Condition
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)

View File

@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class RoR2TestBase(WorldTestBase):
game = "Risk of Rain 2"

View File

@ -0,0 +1,26 @@
from . import RoR2TestBase
class DLCTest(RoR2TestBase):
options = {
"dlc_sotv": "true",
"victory": "any"
}
def test_commencement_victory(self) -> None:
self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
self.assertBeatable(False)
self.collect_by_name("Commencement")
self.assertBeatable(True)
def test_planetarium_victory(self) -> None:
self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
self.assertBeatable(False)
self.collect_by_name("The Planetarium")
self.assertBeatable(True)
def test_moment_whole_victory(self) -> None:
self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"])
self.assertBeatable(False)
self.collect_by_name("Hidden Realm: A Moment, Whole")
self.assertBeatable(True)

View File

@ -0,0 +1,7 @@
from . import RoR2TestBase
class ClassicTest(RoR2TestBase):
options = {
"goal": "classic",
}

View File

@ -0,0 +1,15 @@
from . import RoR2TestBase
class LimboGoalTest(RoR2TestBase):
options = {
"victory": "limbo"
}
def test_limbo(self) -> None:
self.collect_all_but(["Hidden Realm: A Moment, Whole", "Victory"])
self.assertFalse(self.can_reach_entrance("Hidden Realm: A Moment, Whole"))
self.assertBeatable(False)
self.collect_by_name("Hidden Realm: A Moment, Whole")
self.assertTrue(self.can_reach_entrance("Hidden Realm: A Moment, Whole"))
self.assertBeatable(True)

View File

@ -0,0 +1,25 @@
from . import RoR2TestBase
class MithrixGoalTest(RoR2TestBase):
options = {
"victory": "mithrix"
}
def test_mithrix(self) -> None:
self.collect_all_but(["Commencement", "Victory"])
self.assertFalse(self.can_reach_entrance("Commencement"))
self.assertBeatable(False)
self.collect_by_name("Commencement")
self.assertTrue(self.can_reach_entrance("Commencement"))
self.assertBeatable(True)
def test_stage5(self) -> None:
self.collect_all_but(["Stage 4", "Sky Meadow", "Victory"])
self.assertFalse(self.can_reach_entrance("Sky Meadow"))
self.assertBeatable(False)
self.collect_by_name("Sky Meadow")
self.assertFalse(self.can_reach_entrance("Sky Meadow"))
self.collect_by_name("Stage 4")
self.assertTrue(self.can_reach_entrance("Sky Meadow"))
self.assertBeatable(True)

View File

@ -0,0 +1,28 @@
from . import RoR2TestBase
class VoidlingGoalTest(RoR2TestBase):
options = {
"dlc_sotv": "true",
"victory": "voidling"
}
def test_planetarium(self) -> None:
self.collect_all_but(["The Planetarium", "Victory"])
self.assertFalse(self.can_reach_entrance("The Planetarium"))
self.assertBeatable(False)
self.collect_by_name("The Planetarium")
self.assertTrue(self.can_reach_entrance("The Planetarium"))
self.assertBeatable(True)
def test_void_locus_to_victory(self) -> None:
self.collect_all_but(["Void Locus", "Commencement"])
self.assertFalse(self.can_reach_location("Victory"))
self.collect_by_name("Void Locus")
self.assertTrue(self.can_reach_entrance("Victory"))
def test_commencement_to_victory(self) -> None:
self.collect_all_but(["Void Locus", "Commencement"])
self.assertFalse(self.can_reach_location("Victory"))
self.collect_by_name("Commencement")
self.assertTrue(self.can_reach_location("Victory"))