Files
Grinch-AP/worlds/messenger/__init__.py
Aaron Wagener 7193182294 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>
2023-10-10 22:30:20 +02:00

194 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from typing import Any, Dict, List, Optional
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS
from .options import Goal, Logic, MessengerOptions, NotesNeeded, PowerSeals
from .regions import MEGA_SHARDS, REGIONS, REGION_CONNECTIONS, SEALS
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, SHOP_ITEMS, shuffle_shop_prices
from .subclasses import MessengerItem, MessengerRegion
class MessengerWeb(WebWorld):
theme = "ocean"
bug_report_page = "https://github.com/alwaysintreble/TheMessengerRandomizerModAP/issues"
tut_en = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up The Messenger randomizer on your computer.",
"English",
"setup_en.md",
"setup/en",
["alwaysintreble"],
)
tutorials = [tut_en]
class MessengerWorld(World):
"""
As a demon army besieges his village, a young ninja ventures through a cursed world, to deliver a scroll paramount
to his clans survival. What begins as a classic action platformer soon unravels into an expansive time-traveling
adventure full of thrills, surprises, and humor.
"""
game = "The Messenger"
item_name_groups = {
"Notes": set(NOTES),
"Keys": set(NOTES),
"Crest": {"Sun Crest", "Moon Crest"},
"Phobe": set(PHOBEKINS),
"Phobekin": set(PHOBEKINS),
}
options_dataclass = MessengerOptions
options: MessengerOptions
base_offset = 0xADD_000
item_name_to_id = {item: item_id
for item_id, item in enumerate(ALL_ITEMS, base_offset)}
location_name_to_id = {location: location_id
for location_id, location in
enumerate([
*ALWAYS_LOCATIONS,
*[seal for seals in SEALS.values() for seal in seals],
*[shard for shards in MEGA_SHARDS.values() for shard in shards],
*BOSS_LOCATIONS,
*[f"The Shop - {shop_loc}" for shop_loc in SHOP_ITEMS],
*FIGURINES,
"Money Wrench",
], base_offset)}
data_version = 3
required_client_version = (0, 4, 0)
web = MessengerWeb()
total_seals: int = 0
required_seals: int = 0
total_shards: int = 0
shop_prices: Dict[str, int]
figurine_prices: Dict[str, int]
_filler_items: List[str]
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
self.options.shuffle_seals.value = PowerSeals.option_true
self.total_seals = self.options.total_seals.value
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
def create_regions(self) -> None:
for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
if region.name in REGION_CONNECTIONS:
region.add_exits(REGION_CONNECTIONS[region.name])
def create_items(self) -> None:
# create items that are always in the item pool
itempool: List[MessengerItem] = [
self.create_item(item)
for item in self.item_name_to_id
if item not in
{
"Power Seal", *NOTES, *FIGURINES,
*{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]},
} and "Time Shard" not in item
]
if self.options.goal == Goal.option_open_music_box:
# make a list of all notes except those in the player's defined starting inventory, and adjust the
# amount we need to put in the itempool and precollect based on that
notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
self.random.shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \
self.options.notes_needed - \
(len(NOTES) - len(notes))
if precollected_notes_amount:
for note in notes[:precollected_notes_amount]:
self.multiworld.push_precollected(self.create_item(note))
notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes]
elif self.options.goal == Goal.option_power_seal_hunt:
total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool),
self.options.total_seals.value)
if total_seals < self.total_seals:
logging.warning(f"Not enough locations for total seals setting "
f"({self.options.total_seals}). Adjusting to {total_seals}")
self.total_seals = total_seals
self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
for i in range(self.required_seals):
seals[i].classification = ItemClassification.progression_skip_balancing
itempool += seals
remaining_fill = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
if remaining_fill < 10:
self._filler_items = self.random.choices(
list(FILLER)[2:],
weights=list(FILLER.values())[2:],
k=remaining_fill
)
itempool += [self.create_filler() for _ in range(remaining_fill)]
self.multiworld.itempool += itempool
def set_rules(self) -> None:
logic = self.options.logic_level
if logic == Logic.option_normal:
MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard:
MessengerHardRules(self).set_messenger_rules()
else:
MessengerOOBRules(self).set_messenger_rules()
def fill_slot_data(self) -> Dict[str, Any]:
shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
return {
"deathlink": self.options.death_link.value,
"goal": self.options.goal.current_key,
"music_box": self.options.music_box.value,
"required_seals": self.required_seals,
"mega_shards": self.options.shuffle_shards.value,
"logic": self.options.logic_level.current_key,
"shop": shop_prices,
"figures": figure_prices,
"max_price": self.total_shards,
}
def get_filler_item_name(self) -> str:
if not getattr(self, "_filler_items", None):
self._filler_items = [name for name in self.random.choices(
list(FILLER),
weights=list(FILLER.values()),
k=20
)]
return self._filler_items.pop(0)
def create_item(self, name: str) -> MessengerItem:
item_id: Optional[int] = self.item_name_to_id.get(name, None)
override_prog = getattr(self, "multiworld") is not None and \
name in {"Windmill Shuriken"} and \
self.options.logic_level > Logic.option_normal
count = 0
if "Time Shard " in name:
count = int(name.strip("Time Shard ()"))
count = count if count >= 100 else 0
self.total_shards += count
return MessengerItem(name, self.player, item_id, override_prog, count)
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
if item.advancement and "Time Shard" in item.name:
shard_count = int(item.name.strip("Time Shard ()"))
if remove:
shard_count = -shard_count
state.prog_items["Shards", self.player] += shard_count
return super().collect_item(state, item, remove)