Core: move option results to the World class instead of MultiWorld (#993)

🤞 

* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* change Zillion to new options dataclass

* remove unused parameter to function

* use TypeGuard for Literal narrowing

* move dlc quest to new api

* move overcooked 2 to new api

* fixed some missed code in oc2

* - Tried to be compliant with 993 (WIP?)

* - I think it all works now

* - Removed last trace of me touching core

* typo

* It now passes all tests!

* Improve options, fix all issues I hope

* - Fixed init options

* dlcquest: fix bad imports

* missed a file

* - Reduce code duplication

* add as_dict documentation

* - Use .items(), get option name more directly, fix slot data content

* - Remove generic options from the slot data

* improve slot data documentation

* remove `CommonOptions.get_value` (#21)

* better slot data description

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
Aaron Wagener
2023-10-10 15:30:20 -05:00
committed by GitHub
parent a7b4914bb7
commit 7193182294
69 changed files with 1587 additions and 1603 deletions

View File

@@ -6,7 +6,7 @@ from .Rules import set_rules
from .RoR2Environments import *
from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from .Options import ror2_options, ItemWeights
from .Options import ItemWeights, ROR2Options
from worlds.AutoWorld import World, WebWorld
from .Regions import create_regions
@@ -28,8 +28,9 @@ class RiskOfRainWorld(World):
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"
option_definitions = ror2_options
game = "Risk of Rain 2"
options_dataclass = ROR2Options
options: ROR2Options
topology_present = False
item_name_to_id = item_table
@@ -46,45 +47,44 @@ class RiskOfRainWorld(World):
def generate_early(self) -> None:
# figure out how many revivals should exist in the pool
if self.multiworld.goal[self.player] == "classic":
total_locations = self.multiworld.total_locations[self.player].value
if self.options.goal == "classic":
total_locations = self.options.total_locations.value
else:
total_locations = len(
orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value,
shrines=self.multiworld.shrines_per_stage[self.player].value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
scanners=self.multiworld.scanner_per_stage[self.player].value,
altars=self.multiworld.altars_per_stage[self.player].value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
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
)
)
self.total_revivals = int(self.multiworld.total_revivals[self.player].value / 100 *
self.total_revivals = int(self.options.total_revivals.value / 100 *
total_locations)
# self.total_revivals = self.multiworld.total_revivals[self.player].value
if self.multiworld.start_with_revive[self.player].value:
if self.options.start_with_revive:
self.total_revivals -= 1
def create_items(self) -> None:
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend
if self.multiworld.start_with_revive[self.player]:
if self.options.start_with_revive:
self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player))
environments_pool = {}
# only mess with the environments if they are set as items
if self.multiworld.goal[self.player] == "explore":
if self.options.goal == "explore":
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
if self.multiworld.dlc_sotv[self.player]:
if self.options.dlc_sotv:
environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table)
environments_pool = shift_by_offset(environment_vanilla_table, environment_offest)
if self.multiworld.dlc_sotv[self.player]:
if self.options.dlc_sotv:
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
environments_pool = {**environments_pool, **environment_offset_table}
environments_to_precollect = 5 if self.multiworld.begin_with_loop[self.player].value else 1
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)
@@ -100,19 +100,19 @@ class RiskOfRainWorld(World):
for env_name, _ in environments_pool.items():
itempool += [env_name]
if self.multiworld.goal[self.player] == "classic":
if self.options.goal == "classic":
# classic mode
total_locations = self.multiworld.total_locations[self.player].value
total_locations = self.options.total_locations.value
else:
# explore mode
total_locations = len(
orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value,
shrines=self.multiworld.shrines_per_stage[self.player].value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value,
scanners=self.multiworld.scanner_per_stage[self.player].value,
altars=self.multiworld.altars_per_stage[self.player].value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].value
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
)
)
# Create junk items
@@ -138,9 +138,9 @@ class RiskOfRainWorld(World):
def create_junk_pool(self) -> Dict:
# if presets are enabled generate junk_pool from the selected preset
pool_option = self.multiworld.item_weights[self.player].value
pool_option = self.options.item_weights.value
junk_pool: Dict[str, int] = {}
if self.multiworld.item_pool_presets[self.player]:
if self.options.item_pool_presets:
# 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():
@@ -149,31 +149,31 @@ class RiskOfRainWorld(World):
junk_pool = item_pool_weights[pool_option].copy()
else: # generate junk pool from user created presets
junk_pool = {
"Item Scrap, Green": self.multiworld.green_scrap[self.player].value,
"Item Scrap, Red": self.multiworld.red_scrap[self.player].value,
"Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value,
"Item Scrap, White": self.multiworld.white_scrap[self.player].value,
"Common Item": self.multiworld.common_item[self.player].value,
"Uncommon Item": self.multiworld.uncommon_item[self.player].value,
"Legendary Item": self.multiworld.legendary_item[self.player].value,
"Boss Item": self.multiworld.boss_item[self.player].value,
"Lunar Item": self.multiworld.lunar_item[self.player].value,
"Void Item": self.multiworld.void_item[self.player].value,
"Equipment": self.multiworld.equipment[self.player].value
"Item Scrap, Green": self.options.green_scrap.value,
"Item Scrap, Red": self.options.red_scrap.value,
"Item Scrap, Yellow": self.options.yellow_scrap.value,
"Item Scrap, White": self.options.white_scrap.value,
"Common Item": self.options.common_item.value,
"Uncommon Item": self.options.uncommon_item.value,
"Legendary Item": self.options.legendary_item.value,
"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
}
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled
if not (self.multiworld.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic):
if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic):
junk_pool.pop("Lunar Item")
# remove void items from the pool
if not (self.multiworld.dlc_sotv[self.player] or pool_option == ItemWeights.option_void):
if not (self.options.dlc_sotv or pool_option == ItemWeights.option_void):
junk_pool.pop("Void Item")
return junk_pool
def create_regions(self) -> None:
if self.multiworld.goal[self.player] == "classic":
if self.options.goal == "classic":
# classic mode
menu = create_region(self.multiworld, self.player, "Menu")
self.multiworld.regions.append(menu)
@@ -182,7 +182,7 @@ class RiskOfRainWorld(World):
victory_region = create_region(self.multiworld, self.player, "Victory")
self.multiworld.regions.append(victory_region)
petrichor = create_region(self.multiworld, self.player, "Petrichor V",
get_classic_item_pickups(self.multiworld.total_locations[self.player].value))
get_classic_item_pickups(self.options.total_locations.value))
self.multiworld.regions.append(petrichor)
# classic mode can get to victory from the beginning of the game
@@ -200,21 +200,13 @@ class RiskOfRainWorld(World):
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")
return {
"itemPickupStep": self.multiworld.item_pickup_step[self.player].value,
"shrineUseStep": self.multiworld.shrine_use_step[self.player].value,
"goal": self.multiworld.goal[self.player].value,
**options_dict,
"seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)),
"totalLocations": self.multiworld.total_locations[self.player].value,
"chestsPerStage": self.multiworld.chests_per_stage[self.player].value,
"shrinesPerStage": self.multiworld.shrines_per_stage[self.player].value,
"scavengersPerStage": self.multiworld.scavengers_per_stage[self.player].value,
"scannerPerStage": self.multiworld.scanner_per_stage[self.player].value,
"altarsPerStage": self.multiworld.altars_per_stage[self.player].value,
"totalRevivals": self.multiworld.total_revivals[self.player].value,
"startWithDio": self.multiworld.start_with_revive[self.player].value,
"finalStageDeath": self.multiworld.final_stage_death[self.player].value,
"deathLink": self.multiworld.death_link[self.player].value,
}
def create_item(self, name: str) -> Item:
@@ -241,12 +233,12 @@ class RiskOfRainWorld(World):
def create_events(world: MultiWorld, player: int) -> None:
total_locations = world.total_locations[player].value
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.goal[player] == "classic":
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)
@@ -254,7 +246,7 @@ def create_events(world: MultiWorld, player: int) -> None:
event_loc.access_rule = \
lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player)
world_region.locations.append(event_loc)
elif world.goal[player] == "explore":
elif world.worlds[player].options.goal == "explore":
for n in range(1, 6):
event_region = world.get_region(f"OrderedStage_{n}", player)