From 944347a2b3e917d558dfea3e4bc591131424821f Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Sun, 29 Aug 2021 14:02:02 -0400 Subject: [PATCH] Risk of Rain 2 implementation --- worlds/ror2/Items.py | 42 ++++++++++++++++++ worlds/ror2/Locations.py | 23 ++++++++++ worlds/ror2/Options.py | 42 ++++++++++++++++++ worlds/ror2/Rules.py | 32 ++++++++++++++ worlds/ror2/__init__.py | 93 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 worlds/ror2/Items.py create mode 100644 worlds/ror2/Locations.py create mode 100644 worlds/ror2/Options.py create mode 100644 worlds/ror2/Rules.py create mode 100644 worlds/ror2/__init__.py diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py new file mode 100644 index 00000000..35992e1f --- /dev/null +++ b/worlds/ror2/Items.py @@ -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} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py new file mode 100644 index 00000000..eded4b32 --- /dev/null +++ b/worlds/ror2/Locations.py @@ -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": 37999, + "Level One": 37001, + "Level Two": 37002, + "Level Three": 37003, + "Level Four": 37004, + "Level Five": 37005 +} + +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()} diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py new file mode 100644 index 00000000..bb11d0bd --- /dev/null +++ b/worlds/ror2/Options.py @@ -0,0 +1,42 @@ +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 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 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 +} diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py new file mode 100644 index 00000000..c1992be5 --- /dev/null +++ b/worlds/ror2/Rules.py @@ -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) \ No newline at end of file diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py new file mode 100644 index 00000000..cb6da6d4 --- /dev/null +++ b/worlds/ror2/__init__.py @@ -0,0 +1,93 @@ +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): + 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 + + 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] + + # 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_locations[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_locations[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