Merge pull request #58 from Ijwu/main

Risk of Rain 2 support
This commit is contained in:
Fabian Dill 2021-09-01 11:30:41 +00:00 committed by GitHub
commit 2d65fbf798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 342 additions and 0 deletions

View File

@ -8,6 +8,7 @@ Currently, the following games are supported:
* Minecraft
* Subnautica
* Slay the Spire
* Risk of Rain 2
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -82,6 +82,7 @@ def page_not_found(err):
return render_template('404.html'), 404
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):

View File

@ -0,0 +1,66 @@
# Risk of Rain 2 Setup Guide
## Install using r2modman
### Install r2modman
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
https://thunderstore.io/package/ebkr/r2modman/
### Install Archipelago Mod using r2modman
You can install the Archipelago mod using r2modman in one of two ways.
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
https://thunderstore.io/package/ArchipelagoMW/Archipelago/
You can also search for the "Archipelago" mod in the r2modman interface.
The mod manager should automatically install all necessary dependencies as well.
### Running the Modded Game
Click on the "Start modded" button in the top left in r2modman to start the game with the
Archipelago mod installed.
## Joining an Archipelago Session
There will be a menu button on the right side of the screen in the character select menu.
Click it in order to bring up the in lobby mod config.
From here you can expand the Archipelago sections and fill in the relevant info.
Keep password blank if there is no password on the server.
Simply check `Enable Archipelago?` and when you start the run it will automatically connect.
## Gameplay
The Risk of Rain 2 players send checks by causing items to spawn in-game. That means opening chests or killing bosses, generally.
An item check is only sent out after a certain number of items are picked up. This count is configurable in the player's YAML.
## YAML Settings
An example YAML would look like this:
```yaml
description: Ijwu-ror2
name: Ijwu
game:
Risk of Rain 2: 1
Risk of Rain 2:
total_locations: 15
total_items: 30
total_revivals: 4
start_with_revive: true
item_pickup_step: 1
enable_lunar: true
```
| Name | Description | Allowed values |
| ---- | ----------- | -------------- |
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. | 10 - 50 |
| total_items | The total number of items which are added to the multiworld on behalf of the Risk of Rain player. | 10-50 |
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other players. (total_locations = 15)
They will have 30 items which can be granted to them through the multiworld. (total_items = 30)
They will complete a location check every second item. (item_pickup_step = 1)
They will have 4 of the items which other players can grant them replaced with `Dio's Best Friend`. (total_revivals = 4)
The player will also start with a `Dio's Best Friend`. (start_with_revive = true)
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)

View File

@ -139,5 +139,24 @@
]
}
]
},
{
"gameTitle": "Risk of Rain 2",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Risk of Rain 2 integration for Archipelago multiworld games.",
"files": [
{
"language": "English",
"filename": "ror2/setup_en.md",
"link": "ror2/setup/en",
"authors": [
"Ijwu"
]
}
]
}
]
}
]

42
worlds/ror2/Items.py Normal file
View File

@ -0,0 +1,42 @@
from BaseClasses import Item
import typing
class RiskOfRainItem(Item):
game: str = "Risk of Rain 2"
# 37000 - 38000
item_table = {
"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,
"Victory": None,
"Beat Level One": None,
"Beat Level Two": None,
"Beat Level Three": None,
"Beat Level Four": None,
"Beat Level Five": None,
}
junk_weights = {
"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,
"Equipment": 32,
}
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id}

23
worlds/ror2/Locations.py Normal file
View File

@ -0,0 +1,23 @@
from BaseClasses import Location
import typing
class RiskOfRainLocation(Location):
game: str = "Risk of Rain 2"
# 37000 - 38000
base_location_table = {
"Victory": None,
"Level One": None,
"Level Two": None,
"Level Three": None,
"Level Four": None,
"Level Five": None
}
item_pickups = {
f"ItemPickup{i}": 37005+i for i in range(1, 51)
}
location_table = {**base_location_table, **item_pickups}
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in location_table.items()}

55
worlds/ror2/Options.py Normal file
View File

@ -0,0 +1,55 @@
import typing
from Options import Option, Toggle, Range
class TotalLocations(Range):
"""Number of location checks which are added to the Risk of Rain playthrough."""
displayname = "Total Locations"
range_start = 10
range_end = 50
default = 15
class TotalItems(Range):
"""Number of items which are added to the multiworld on behalf of the Risk of Rain player."""
displayname = "Total Items"
range_start = 10
range_end = 50
default = 30
class TotalRevivals(Range):
"""Number of `Dio's Best Friend` item put in the item pool."""
displayname = "Total Revivals Available"
range_start = 0
range_end = 10
default = 4
class ItemPickupStep(Range):
"""Number of items to pick up before an AP Check is completed.
Setting to 1 means every other pickup.
Setting to 2 means every third pickup. So on..."""
displayname = "Item Pickup Step"
range_start = 0
range_end = 5
default = 1
class AllowLunarItems(Toggle):
"""Allows Lunar items in the item pool."""
displayname = "Enable Lunar Item Shuffling"
default = True
class StartWithRevive(Toggle):
"""Start the game with a `Dio's Best Friend` item."""
displayname = "Start with a Revive"
default = True
ror2_options: typing.Dict[str, type(Option)] = {
"total_locations": TotalLocations,
"total_revivals": TotalRevivals,
"start_with_revive": StartWithRevive,
"item_pickup_step": ItemPickupStep,
"total_items": TotalItems,
"enable_lunar": AllowLunarItems
}

32
worlds/ror2/Rules.py Normal file
View File

@ -0,0 +1,32 @@
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
class RiskOfRainLogic(LogicMixin):
def _ror_has_items(self, player: int, amount: int) -> bool:
count: int = self.item_count("Common Item", player) + self.item_count("Uncommon Item", player) + \
self.item_count("Legendary Item", player) + self.item_count("Boss Item", player)
return count >= amount
def set_rules(world: MultiWorld, player: int):
total_checks = world.total_locations[player]
# divide by 5 since 5 levels (then commencement)
items_per_level = total_checks / 5
leftover = total_checks % 5
set_rule(world.get_location("Level One", player),
lambda state: state._ror_has_items(player, items_per_level + leftover))
set_rule(world.get_location("Level Two", player),
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level One", player))
set_rule(world.get_location("Level Three", player),
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Two", player))
set_rule(world.get_location("Level Four", player),
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Three", player))
set_rule(world.get_location("Level Five", player),
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Four", player))
set_rule(world.get_location("Victory", player),
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Five", player))
world.completion_condition[player] = lambda state: state.has("Victory", player)

103
worlds/ror2/__init__.py Normal file
View File

@ -0,0 +1,103 @@
import string
from .Items import RiskOfRainItem, item_table, junk_weights
from .Locations import location_table, RiskOfRainLocation, base_location_table
from .Rules import set_rules
from BaseClasses import Region, Entrance, Item, MultiWorld
from .Options import ror2_options
from ..AutoWorld import World
client_version = 1
class RiskOfRainWorld(World):
"""
Escape a chaotic alien planet by fighting through hordes of frenzied monsters with your friends, or on your own.
Combine loot in surprising ways and master each character until you become the havoc you feared upon your
first crash landing.
"""
game: str = "Risk of Rain 2"
options = ror2_options
topology_present = False
item_name_to_id = {name: data for name, data in item_table.items()}
location_name_to_id = {name: data for name, data in location_table.items()}
data_version = 1
forced_auto_forfeit = True
def generate_basic(self):
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
if self.world.start_with_revive[self.player].value:
self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player))
# Generate item pool
itempool = []
junk_pool = junk_weights.copy()
# Add revive items for the player
itempool += ["Dio's Best Friend"] * self.world.total_revivals[self.player]
if not self.world.enable_lunar[self.player]:
junk_pool.pop("Lunar Item")
# Fill remaining items with randomly generated junk
itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
k=self.world.total_items[self.player] -
self.world.total_revivals[self.player])
# Convert itempool into real items
itempool = [item for item in map(lambda name: self.create_item(name), itempool)]
self.world.itempool += itempool
def set_rules(self):
set_rules(self.world, self.player)
def create_regions(self):
create_regions(self.world, self.player)
def fill_slot_data(self):
return {
"itemPickupStep": self.world.item_pickup_step[self.player].value,
"seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)),
"totalLocations": self.world.total_items[self.player].value,
"totalRevivals": self.world.total_revivals[self.player].value
}
def create_item(self, name: str) -> Item:
item_id = item_table[name]
item = RiskOfRainItem(name, True, item_id, self.player)
return item
def create_regions(world, player: int):
world.regions += [
create_region(world, player, 'Menu', None, ['Lobby']),
create_region(world, player, 'Petrichor V',
[location for location in base_location_table] +
[f"ItemPickup{i}" for i in range(1, world.total_locations[player])])
]
world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player))
world.get_location("Level One", player).place_locked_item(RiskOfRainItem("Beat Level One", True, None, player))
world.get_location("Level Two", player).place_locked_item(RiskOfRainItem("Beat Level Two", True, None, player))
world.get_location("Level Three", player).place_locked_item(RiskOfRainItem("Beat Level Three", True, None, player))
world.get_location("Level Four", player).place_locked_item(RiskOfRainItem("Beat Level Four", True, None, player))
world.get_location("Level Five", player).place_locked_item(RiskOfRainItem("Beat Level Five", True, None, player))
world.get_location("Victory", player).place_locked_item(RiskOfRainItem("Victory", True, None, player))
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = location_table.get(location, 0)
location = RiskOfRainLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret