From 56c2272bfd7921bd1f42577583295ffd053589e3 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Tue, 10 Mar 2026 13:05:59 -0600 Subject: [PATCH] RoR2: Seekers of the Storm (SOTS) DLC Support (#5569) --- worlds/ror2/__init__.py | 48 +++++++++++++--- worlds/ror2/archipelago.json | 6 ++ worlds/ror2/docs/en_Risk of Rain 2.md | 11 +++- worlds/ror2/docs/setup_en.md | 8 +++ worlds/ror2/locations.py | 14 ++++- worlds/ror2/options.py | 38 ++++++++++++- worlds/ror2/regions.py | 75 ++++++++++++++++++++---- worlds/ror2/ror2environments.py | 59 ++++++++++++++++--- worlds/ror2/rules.py | 79 ++++++++++++++------------ worlds/ror2/test/test_any_goal.py | 18 ++++-- worlds/ror2/test/test_falseson_goal.py | 17 ++++++ 11 files changed, 302 insertions(+), 71 deletions(-) create mode 100644 worlds/ror2/archipelago.json create mode 100644 worlds/ror2/test/test_falseson_goal.py diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 7873ae54bb..f52ff789eb 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -4,7 +4,10 @@ from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler 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 + environment_sotv_orderedstages_table, environment_sotv_table, environment_sost_orderedstages_table, \ + environment_sost_table, collapse_dict_list_vertical, shift_by_offset, environment_vanilla_variants_table, \ + environment_vanilla_variant_orderedstages_table, environment_sots_variants_table, \ + environment_sots_variants_orderedstages_table from BaseClasses import Item, ItemClassification, Tutorial from .options import ItemWeights, ROR2Options, ror2_option_groups @@ -46,7 +49,7 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - required_client_version = (0, 5, 0) + required_client_version = (0, 6, 4) web = RiskOfWeb() total_revivals: int @@ -62,7 +65,9 @@ class RiskOfRainWorld(World): scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * @@ -71,6 +76,8 @@ class RiskOfRainWorld(World): self.total_revivals -= 1 if self.options.victory == "voidling" and not self.options.dlc_sotv: self.options.victory.value = self.options.victory.option_any + if self.options.victory == "falseson" and not self.options.dlc_sots: + self.options.victory.value = self.options.victory.option_any def create_regions(self) -> None: @@ -105,16 +112,39 @@ class RiskOfRainWorld(World): # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) + # Vanilla Variants + if self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_vanilla_variant_orderedstages_table) if self.options.dlc_sotv: environment_available_orderedstages_table = \ collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + if self.options.dlc_sots: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sost_orderedstages_table) + if self.options.dlc_sots and self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sots_variants_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) - + if self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_vanilla_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} + if self.options.dlc_sots: + environment_offset_table = shift_by_offset(environment_sost_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # SOTS Variant Environments + if self.options.dlc_sots and self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_sots_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # percollect starting environment for stage 1 unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) @@ -146,7 +176,9 @@ class RiskOfRainWorld(World): scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) # Create junk items @@ -223,7 +255,7 @@ class RiskOfRainWorld(World): "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", "require_stages", - "progressive_stages", casing="camel") + "progressive_stages", "stage_variants", "show_seer_portals", casing="camel") return { **options_dict, "seed": "".join(self.random.choice(string.digits) for _ in range(16)), @@ -254,7 +286,7 @@ class RiskOfRainWorld(World): 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) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) or state.has("Helminth Hatchery", self.player) victory_region = self.multiworld.get_region("Victory", self.player) victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) diff --git a/worlds/ror2/archipelago.json b/worlds/ror2/archipelago.json new file mode 100644 index 0000000000..78c54c1420 --- /dev/null +++ b/worlds/ror2/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Risk of Rain 2", + "minimum_ap_version": "0.6.4", + "world_version": "1.5.0", + "authors": ["Kindasneaki"] +} \ No newline at end of file diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index 651c89a339..2acd133e26 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -88,12 +88,21 @@ Explore Mode items are: * `Commencement` * `All the Hidden Realms` -Dlc_Sotv items +DLC Survivors of the Void (SOTV) items * `Siphoned Forest` * `Aphelian Sanctuary` * `Sulfur Pools` * `Void Locus` +DLC Seekers of the Storm (SOTS) items + +* `Shattered Abodes`, `Vicious Falls`, `Disturbed Impact` +* `Reformed Altar` +* `Treeborn Colony`, `Golden Dieback` +* `Prime Meridian` +* `Helminth Hatchery` + + When an explore item is granted, it will unlock that environment and will now be accessible! The game will still pick randomly which environment is next, but it will first check to see if they are available. If you have multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 6acf2654a8..cef0885970 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -23,6 +23,13 @@ all necessary dependencies as well. Click on the `Start modded` button in the top left in `r2modman` to start the game with the Archipelago mod installed. +### Troubleshooting + +* The mod doesn't show up in game! + * `r2modman` looks for the game at its default directory. If you have the game installed somewhere else, + you can update `r2modman` by going to `Settings > Change Risk of Rain 2 folder` + and selecting the correct directory. + ## Configuring your YAML File ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn @@ -59,6 +66,7 @@ also optionally connect to the multiworld using the text client, which can be fo ### In-Game Commands These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_reconnect` Reconnect to AP. - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". - `archipelago_deathlink true/false` Toggle deathlink. - `archipelago_disconnect` Disconnect from AP. diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py index 13077b3e14..3297231152 100644 --- a/worlds/ror2/locations.py +++ b/worlds/ror2/locations.py @@ -3,7 +3,8 @@ 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 + environment_sotv_orderedstages_table, environment_sost_orderedstages_table, \ + environment_sots_variants_orderedstages_table, environment_vanilla_variant_orderedstages_table class RiskOfRainLocation(Location): @@ -57,13 +58,20 @@ def get_environment_locations(chests: int, shrines: int, scavengers: int, scanne return locations -def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool, + dlc_sots: bool, stage_variants: 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 stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)) if dlc_sotv: orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + if dlc_sots: + orderedstages.update(compress_dict_list_horizontal(environment_sost_orderedstages_table)) + if dlc_sots and stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)) # for every environment, generate the respective locations for environment_name, environment_index in orderedstages.items(): locations.update(get_environment_locations( @@ -86,4 +94,6 @@ location_table.update(get_locations( scanners=ScannersPerEnvironment.range_end, altars=AltarsPerEnvironment.range_end, dlc_sotv=True, + dlc_sots=True, + stage_variants=True )) diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 381c5942b0..876a67b7fb 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -22,8 +22,9 @@ class Goal(Choice): class Victory(Choice): """ Mithrix: Defeat Mithrix in Commencement - Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Voidling: Defeat the Voidling in The Planetarium (SOTV DLC required! Will select any if not enabled.) Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Falseson: Defeat False son and gift an item to the altar in Prime Meridian (SOTS DLC required! Will select any if not enabled.) Any: Any victory in the game will count. See Final Stage Death for additional ways. """ display_name = "Victory Condition" @@ -31,6 +32,7 @@ class Victory(Choice): option_mithrix = 1 option_voidling = 2 option_limbo = 3 + option_falseson = 4 default = 0 @@ -138,18 +140,26 @@ class FinalStageDeath(Toggle): 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.""" + Victory: limbo - Obliterating yourself will count. + Victory: falseson - only dying in Prime Meridian will count.""" display_name = "Final Stage Death is Win" class DLC_SOTV(Toggle): """ - Enable if you are using SOTV DLC. + Enable if you are using Survivors of the Void DLC. Affects environment availability for Explore Mode. Adds Void Items into the item pool """ display_name = "Enable DLC - SOTV" +class DLC_SOTS(Toggle): + """ + Enable if you are using Seekers of the Storm DLC. + Affects environment availability for Explore Mode. + """ + display_name = "Enable DLC - SOTS" + class RequireStages(DefaultOnToggle): """Add Stage items to the pool to block access to the next set of environments.""" @@ -162,6 +172,23 @@ class ProgressiveStages(DefaultOnToggle): display_name = "Progressive Stages" +class StageVariants(Toggle): + """Enable if you want to include stage variants in the environment pool. + Stages included are: + - Distant Roost (2) + - Titanic Plains (2) + SOTS DLC Enabled: + - Vicious Falls + - Shattered Abodes + - Golden Dieback""" + display_name = "Include Stage Variants" + + +class ShowSeerPortals(DefaultOnToggle): + """Shows Seer Portals at the teleporter to allow choosing the next environment.""" + display_name = "Show Seer Portals" + + class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -384,6 +411,8 @@ ror2_option_groups = [ AltarsPerEnvironment, RequireStages, ProgressiveStages, + StageVariants, + ShowSeerPortals, ]), OptionGroup("Classic Mode Options", [ TotalLocations, @@ -427,8 +456,11 @@ class ROR2Options(PerGameCommonOptions): start_with_revive: StartWithRevive final_stage_death: FinalStageDeath dlc_sotv: DLC_SOTV + dlc_sots: DLC_SOTS require_stages: RequireStages progressive_stages: ProgressiveStages + stage_variants: StageVariants + show_seer_portals: ShowSeerPortals death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep diff --git a/worlds/ror2/regions.py b/worlds/ror2/regions.py index def29b4728..780f66bcac 100644 --- a/worlds/ror2/regions.py +++ b/worlds/ror2/regions.py @@ -18,13 +18,10 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: 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", "Titanic Plains", "Verdant Falls"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), - "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), - "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), "Verdant Falls": RoRRegionData([], ["OrderedStage_1"]), "Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]), "Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]), @@ -35,12 +32,30 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Sundered Grove": RoRRegionData([], ["OrderedStage_4"]), "Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), } + non_dlc_variant_regions: Dict[str, RoRRegionData] = { + "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), + "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), + } # SOTV Regions - dlc_regions: Dict[str, RoRRegionData] = { + dlc_sotv_regions: Dict[str, RoRRegionData] = { "Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]), "Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]), "Sulfur Pools": RoRRegionData([], ["OrderedStage_3"]) } + + dlc_sost_regions: Dict[str, RoRRegionData] = { + "Shattered Abodes": RoRRegionData([], ["OrderedStage_1"]), + "Reformed Altar": RoRRegionData([], ["OrderedStage_2", "Treeborn Colony"]), + "Treeborn Colony": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + "Helminth Hatchery": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), + } + + dlc_sots_variant_regions: Dict[str, RoRRegionData] = { + "Viscous Falls": RoRRegionData([], ["OrderedStage_1"]), + "Disturbed Impact": RoRRegionData([], ["OrderedStage_1"]), + "Golden Dieback": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + } + other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", @@ -61,10 +76,15 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } - dlc_other_regions: Dict[str, RoRRegionData] = { + dlc_sotv_other_regions: Dict[str, RoRRegionData] = { "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } + + dlc_sost_other_regions: Dict[str, RoRRegionData] = { + "Prime Meridian": RoRRegionData(None, ["Victory", "Petrichor V"]), + } + # Totals of each item chests = int(ror2_options.chests_per_stage) shrines = int(ror2_options.shrines_per_stage) @@ -72,8 +92,14 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: scanners = int(ror2_options.scanner_per_stage) newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} + if ror2_options.stage_variants: + all_location_regions.update(non_dlc_variant_regions) if ror2_options.dlc_sotv: - all_location_regions = {**non_dlc_regions, **dlc_regions} + all_location_regions.update(dlc_sotv_regions) + if ror2_options.dlc_sots: + all_location_regions.update(dlc_sost_regions) + if ror2_options.dlc_sots and ror2_options.stage_variants: + all_location_regions.update(dlc_sots_variant_regions) # Locations for key in all_location_regions: @@ -99,25 +125,52 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}") regions_pool: Dict = {**all_location_regions, **other_regions} - # DLC Locations + # Non DLC Variant Locations + if ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Distant Roost (2)") + non_dlc_regions["Menu"].region_exits.append("Titanic Plains (2)") + # SOTV DLC Locations 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} + + # SOTS DLC Locations + if ror2_options.dlc_sots: + non_dlc_regions["Menu"].region_exits.append("Shattered Abodes") + other_regions["OrderedStage_1"].region_exits.append("Reformed Altar") + other_regions["OrderedStage_4"].region_exits.append("Helminth Hatchery") + + # SOTS Variant Locations + if ror2_options.dlc_sots and ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Viscous Falls") + non_dlc_regions["Menu"].region_exits.append("Disturbed Impact") + dlc_sost_regions["Reformed Altar"].region_exits.append("Golden Dieback") + + if ror2_options.dlc_sotv: + regions_pool.update(dlc_sotv_other_regions) + if ror2_options.dlc_sots: + regions_pool.update(dlc_sost_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) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].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) + dlc_sost_other_regions["Prime Meridian"].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) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) + elif ror2_options.victory == "falseson": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) # Create all the regions for name, data in regions_pool.items(): diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py index 61707b3362..40e63a35b1 100644 --- a/worlds/ror2/ror2environments.py +++ b/worlds/ror2/ror2environments.py @@ -4,11 +4,14 @@ from typing import Dict, List, TypeVar 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 "Verdant Falls": 28, # lakes } +environment_vanilla_variant_orderedstage_1_table: Dict[str, int] = { + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains (2)": 16, # golemplains2 +} + environment_vanilla_orderedstage_2_table: Dict[str, int] = { "Abandoned Aqueduct": 17, # goolake "Wetland Aspect": 12, # foggyswamp @@ -54,6 +57,34 @@ environment_sotv_special_table: Dict[str, int] = { "The Planetarium": 45, # voidraid } +environment_sost_orderstage_1_table: Dict[str, int] = { + "Shattered Abodes": 54, # village + +} +environment_sost_variant_orderstage_1_table: Dict[str, int] = { + "Viscous Falls": 34, # lakesnight + "Disturbed Impact": 55, # villagenight +} + +environment_sost_orderstage_2_table: Dict[str, int] = { + "Reformed Altar": 36, # lemuriantemple +} + +environment_sost_orderstage_3_table: Dict[str, int] = { + "Treeborn Colony": 21, # habitat +} +environment_sost_variant_orderstage_3_table: Dict[str, int] = { + "Golden Dieback": 22, # habitatfall +} + +environment_sost_orderstage_5_table: Dict[str, int] = { + "Helminth Hatchery": 23, # helminthroost +} + +environment_sost_special_table: Dict[str, int] = { + "Prime Meridian": 40, # meridian +} + X = TypeVar("X") Y = TypeVar("Y") @@ -100,18 +131,32 @@ environment_vanilla_orderedstages_table = \ environment_vanilla_table = \ {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} +# Vanilla Variants +environment_vanilla_variant_orderedstages_table = \ + [environment_vanilla_variant_orderedstage_1_table] +environment_vanilla_variants_table = \ + {**compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)} +# SoTV 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} +# SoST +environment_sost_orderedstages_table = \ + [environment_sost_orderstage_1_table, environment_sost_orderstage_2_table, + environment_sost_orderstage_3_table, {}, environment_sost_orderstage_5_table] # There is no new stage 4 in SoST +environment_sost_table = \ + {**compress_dict_list_horizontal(environment_sost_orderedstages_table), **environment_sost_special_table} +# SOTS Variants +environment_sots_variants_orderedstages_table = \ + [environment_sost_variant_orderstage_1_table, {}, environment_sost_variant_orderstage_3_table] +environment_sots_variants_table = \ + {**compress_dict_list_horizontal(environment_sots_variants_orderedstages_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} +environment_all_table = {**environment_vanilla_table, **environment_sotv_table, **environment_sost_table, + **environment_vanilla_variants_table, **environment_sots_variants_table} def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py index f0ab9f2831..d8d92ca270 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -1,7 +1,9 @@ from worlds.generic.Rules import set_rule, add_rule from BaseClasses import MultiWorld from .locations import get_locations -from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ + environment_sost_orderedstages_table, environment_vanilla_variant_orderedstages_table, \ + environment_sots_variants_orderedstages_table from typing import Set, TYPE_CHECKING if TYPE_CHECKING: @@ -43,6 +45,24 @@ def has_location_access_rule(multiworld: MultiWorld, environment: str, player: i multiworld.get_location(location_name, player).access_rule = \ lambda state: state.has(environment, player) +def explore_environment_location_rules(table, multiworld, player, chests, shrines, newts, scavengers, scanners): + for i in range(len(table)): + for environment_name, _ in 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_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + def set_rules(ror2_world: "RiskOfRainWorld") -> None: player = ror2_world.player @@ -60,7 +80,9 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: 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) + dlc_sotv=bool(ror2_options.dlc_sotv.value), + dlc_sots=bool(ror2_options.dlc_sots.value), + stage_variants=bool(ror2_options.stage_variants) ) ) @@ -101,40 +123,25 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: 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 - 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_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) - + # Vanilla stages + explore_environment_location_rules(environment_vanilla_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # Vanilla Variant stages + if ror2_options.stage_variants: + explore_environment_location_rules(environment_vanilla_variant_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # SoTv stages 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 - 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_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + explore_environment_location_rules(environment_sotv_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + # SoTS stages + if ror2_options.dlc_sots: + explore_environment_location_rules(environment_sost_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + if ror2_options.dlc_sots and ror2_options.stage_variants: + explore_environment_location_rules(environment_sots_variants_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player) @@ -147,6 +154,8 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: 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) + if ror2_options.dlc_sots: + has_entrance_access_rule(multiworld, "Stage 5", "Prime Meridian", player) # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py index 18d4994419..7dbd549c9f 100644 --- a/worlds/ror2/test/test_any_goal.py +++ b/worlds/ror2/test/test_any_goal.py @@ -4,23 +4,33 @@ from . import RoR2TestBase class DLCTest(RoR2TestBase): options = { "dlc_sotv": "true", - "victory": "any" + "victory": "any", + "dlc_sots": "true", } def test_commencement_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "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.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "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.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("Hidden Realm: A Moment, Whole") self.assertBeatable(True) + def test_false_son_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_falseson_goal.py b/worlds/ror2/test/test_falseson_goal.py new file mode 100644 index 0000000000..3cf815c942 --- /dev/null +++ b/worlds/ror2/test/test_falseson_goal.py @@ -0,0 +1,17 @@ +from . import RoR2TestBase + + +class FalseSonGoalTest(RoR2TestBase): + options = { + "dlc_sots": "true", + "victory": "falseson", + "stage_variants": "true" + } + + def test_false_son(self) -> None: + self.collect_all_but(["Prime Meridian", "Victory"]) + self.assertFalse(self.can_reach_region("Prime Meridian")) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertTrue(self.can_reach_region("Prime Meridian")) + self.assertBeatable(True)