mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Timespinner: migrate to new options api and correct random (#2485)
* Implemented new options system into Timespinner * Fixed typo * Fixed typo * Fixed slotdata maybe * Fixes * more fixes * Fixed failing unit tests * Implemented options backwards comnpatibility * Fixed option fallbacks * Implemented review results * Fixed logic bug * Fixed python 3.8/3.9 compatibility * Replaced one more multiworld option usage * Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
from typing import Dict, List, Set, Tuple, TextIO, Union
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from typing import Dict, List, Set, Tuple, TextIO
|
||||
from BaseClasses import Item, Tutorial, ItemClassification
|
||||
from .Items import get_item_names_per_category
|
||||
from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items
|
||||
from .Locations import get_location_datas, EventId
|
||||
from .Options import is_option_enabled, get_option_value, timespinner_options
|
||||
from .Options import BackwardsCompatiableTimespinnerOptions, Toggle
|
||||
from .PreCalculatedWeights import PreCalculatedWeights
|
||||
from .Regions import create_regions_and_locations
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
import logging
|
||||
|
||||
class TimespinnerWebWorld(WebWorld):
|
||||
theme = "ice"
|
||||
@@ -35,32 +36,34 @@ class TimespinnerWorld(World):
|
||||
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
|
||||
Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family.
|
||||
"""
|
||||
|
||||
option_definitions = timespinner_options
|
||||
options_dataclass = BackwardsCompatiableTimespinnerOptions
|
||||
options: BackwardsCompatiableTimespinnerOptions
|
||||
game = "Timespinner"
|
||||
topology_present = True
|
||||
web = TimespinnerWebWorld()
|
||||
required_client_version = (0, 4, 2)
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)}
|
||||
location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)}
|
||||
item_name_groups = get_item_names_per_category()
|
||||
|
||||
precalculated_weights: PreCalculatedWeights
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player)
|
||||
self.options.handle_backward_compatibility()
|
||||
|
||||
self.precalculated_weights = PreCalculatedWeights(self.options, self.random)
|
||||
|
||||
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
|
||||
if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
|
||||
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
|
||||
if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0:
|
||||
self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true
|
||||
if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
|
||||
self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true
|
||||
if self.options.start_inventory.value.pop('Meyef', 0) > 0:
|
||||
self.options.start_with_meyef.value = Toggle.option_true
|
||||
if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0:
|
||||
self.options.quick_seed.value = Toggle.option_true
|
||||
if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0:
|
||||
self.options.start_with_jewelry_box.value = Toggle.option_true
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights)
|
||||
create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights)
|
||||
|
||||
def create_items(self) -> None:
|
||||
self.create_and_assign_event_items()
|
||||
@@ -74,7 +77,7 @@ class TimespinnerWorld(World):
|
||||
|
||||
def set_rules(self) -> None:
|
||||
final_boss: str
|
||||
if self.is_option_enabled("DadPercent"):
|
||||
if self.options.dad_percent:
|
||||
final_boss = "Killed Emperor"
|
||||
else:
|
||||
final_boss = "Killed Nightmare"
|
||||
@@ -82,48 +85,74 @@ class TimespinnerWorld(World):
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, object]:
|
||||
slot_data: Dict[str, object] = {}
|
||||
|
||||
ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"}
|
||||
|
||||
for option_name in timespinner_options:
|
||||
if (option_name not in ap_specific_settings):
|
||||
slot_data[option_name] = self.get_option_value(option_name)
|
||||
|
||||
slot_data["StinkyMaw"] = True
|
||||
slot_data["ProgressiveVerticalMovement"] = False
|
||||
slot_data["ProgressiveKeycards"] = False
|
||||
slot_data["PersonalItems"] = self.get_personal_items()
|
||||
slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock
|
||||
slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock
|
||||
slot_data["PastGate"] = self.precalculated_weights.past_key_unlock
|
||||
slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock
|
||||
slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \
|
||||
int(self.precalculated_weights.flood_basement_high)
|
||||
slot_data["Xarion"] = self.precalculated_weights.flood_xarion
|
||||
slot_data["Maw"] = self.precalculated_weights.flood_maw
|
||||
slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft
|
||||
slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back
|
||||
slot_data["CastleMoat"] = self.precalculated_weights.flood_moat
|
||||
slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard
|
||||
slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation
|
||||
slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene
|
||||
slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge
|
||||
slot_data["Lab"] = self.precalculated_weights.flood_lab
|
||||
|
||||
return slot_data
|
||||
return {
|
||||
# options
|
||||
"StartWithJewelryBox": self.options.start_with_jewelry_box.value,
|
||||
"DownloadableItems": self.options.downloadable_items.value,
|
||||
"EyeSpy": self.options.eye_spy.value,
|
||||
"StartWithMeyef": self.options.start_with_meyef.value,
|
||||
"QuickSeed": self.options.quick_seed.value,
|
||||
"SpecificKeycards": self.options.specific_keycards.value,
|
||||
"Inverted": self.options.inverted.value,
|
||||
"GyreArchives": self.options.gyre_archives.value,
|
||||
"Cantoran": self.options.cantoran.value,
|
||||
"LoreChecks": self.options.lore_checks.value,
|
||||
"BossRando": self.options.boss_rando.value,
|
||||
"DamageRando": self.options.damage_rando.value,
|
||||
"DamageRandoOverrides": self.options.damage_rando_overrides.value,
|
||||
"HpCap": self.options.hp_cap.value,
|
||||
"LevelCap": self.options.level_cap.value,
|
||||
"ExtraEarringsXP": self.options.extra_earrings_xp.value,
|
||||
"BossHealing": self.options.boss_healing.value,
|
||||
"ShopFill": self.options.shop_fill.value,
|
||||
"ShopWarpShards": self.options.shop_warp_shards.value,
|
||||
"ShopMultiplier": self.options.shop_multiplier.value,
|
||||
"LootPool": self.options.loot_pool.value,
|
||||
"DropRateCategory": self.options.drop_rate_category.value,
|
||||
"FixedDropRate": self.options.fixed_drop_rate.value,
|
||||
"LootTierDistro": self.options.loot_tier_distro.value,
|
||||
"ShowBestiary": self.options.show_bestiary.value,
|
||||
"ShowDrops": self.options.show_drops.value,
|
||||
"EnterSandman": self.options.enter_sandman.value,
|
||||
"DadPercent": self.options.dad_percent.value,
|
||||
"RisingTides": self.options.rising_tides.value,
|
||||
"UnchainedKeys": self.options.unchained_keys.value,
|
||||
"PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value,
|
||||
"Traps": self.options.traps.value,
|
||||
"DeathLink": self.options.death_link.value,
|
||||
"StinkyMaw": True,
|
||||
# data
|
||||
"PersonalItems": self.get_personal_items(),
|
||||
"PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock,
|
||||
"PresentGate": self.precalculated_weights.present_key_unlock,
|
||||
"PastGate": self.precalculated_weights.past_key_unlock,
|
||||
"TimeGate": self.precalculated_weights.time_key_unlock,
|
||||
# rising tides
|
||||
"Basement": int(self.precalculated_weights.flood_basement) + \
|
||||
int(self.precalculated_weights.flood_basement_high),
|
||||
"Xarion": self.precalculated_weights.flood_xarion,
|
||||
"Maw": self.precalculated_weights.flood_maw,
|
||||
"PyramidShaft": self.precalculated_weights.flood_pyramid_shaft,
|
||||
"BackPyramid": self.precalculated_weights.flood_pyramid_back,
|
||||
"CastleMoat": self.precalculated_weights.flood_moat,
|
||||
"CastleCourtyard": self.precalculated_weights.flood_courtyard,
|
||||
"LakeDesolation": self.precalculated_weights.flood_lake_desolation,
|
||||
"DryLakeSerene": not self.precalculated_weights.flood_lake_serene,
|
||||
"LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge,
|
||||
"Lab": self.precalculated_weights.flood_lab
|
||||
}
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
if self.is_option_enabled("UnchainedKeys"):
|
||||
if self.options.unchained_keys:
|
||||
spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n')
|
||||
spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n')
|
||||
|
||||
if self.is_option_enabled("EnterSandman"):
|
||||
if self.options.enter_sandman:
|
||||
spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n')
|
||||
else:
|
||||
spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n')
|
||||
|
||||
if self.is_option_enabled("RisingTides"):
|
||||
if self.options.rising_tides:
|
||||
flooded_areas: List[str] = []
|
||||
|
||||
if self.precalculated_weights.flood_basement:
|
||||
@@ -159,6 +188,15 @@ class TimespinnerWorld(World):
|
||||
|
||||
spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n')
|
||||
|
||||
if self.options.has_replaced_options:
|
||||
warning = \
|
||||
f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \
|
||||
"please update your yaml"
|
||||
|
||||
spoiler_handle.write("\n")
|
||||
spoiler_handle.write(warning)
|
||||
logging.warning(warning)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = item_table[name]
|
||||
|
||||
@@ -176,41 +214,41 @@ class TimespinnerWorld(World):
|
||||
if not item.advancement:
|
||||
return item
|
||||
|
||||
if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"):
|
||||
if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items:
|
||||
item.classification = ItemClassification.filler
|
||||
elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"):
|
||||
elif name == 'Oculus Ring' and not self.options.eye_spy:
|
||||
item.classification = ItemClassification.filler
|
||||
elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"):
|
||||
elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives:
|
||||
item.classification = ItemClassification.filler
|
||||
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
|
||||
and not self.is_option_enabled("UnchainedKeys"):
|
||||
and not self.options.unchained_keys:
|
||||
item.classification = ItemClassification.filler
|
||||
|
||||
return item
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
trap_chance: int = self.get_option_value("TrapChance")
|
||||
enabled_traps: List[str] = self.get_option_value("Traps")
|
||||
trap_chance: int = self.options.trap_chance.value
|
||||
enabled_traps: List[str] = self.options.traps.value
|
||||
|
||||
if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps:
|
||||
return self.multiworld.random.choice(enabled_traps)
|
||||
if self.random.random() < (trap_chance / 100) and enabled_traps:
|
||||
return self.random.choice(enabled_traps)
|
||||
else:
|
||||
return self.multiworld.random.choice(filler_items)
|
||||
return self.random.choice(filler_items)
|
||||
|
||||
def get_excluded_items(self) -> Set[str]:
|
||||
excluded_items: Set[str] = set()
|
||||
|
||||
if self.is_option_enabled("StartWithJewelryBox"):
|
||||
if self.options.start_with_jewelry_box:
|
||||
excluded_items.add('Jewelry Box')
|
||||
if self.is_option_enabled("StartWithMeyef"):
|
||||
if self.options.start_with_meyef:
|
||||
excluded_items.add('Meyef')
|
||||
if self.is_option_enabled("QuickSeed"):
|
||||
if self.options.quick_seed:
|
||||
excluded_items.add('Talaria Attachment')
|
||||
|
||||
if self.is_option_enabled("UnchainedKeys"):
|
||||
if self.options.unchained_keys:
|
||||
excluded_items.add('Twin Pyramid Key')
|
||||
|
||||
if not self.is_option_enabled("EnterSandman"):
|
||||
if not self.options.enter_sandman:
|
||||
excluded_items.add('Mysterious Warp Beacon')
|
||||
else:
|
||||
excluded_items.add('Timeworn Warp Beacon')
|
||||
@@ -224,8 +262,8 @@ class TimespinnerWorld(World):
|
||||
return excluded_items
|
||||
|
||||
def assign_starter_items(self, excluded_items: Set[str]) -> None:
|
||||
non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value
|
||||
local_items: Set[str] = self.multiworld.local_items[self.player].value
|
||||
non_local_items: Set[str] = self.options.non_local_items.value
|
||||
local_items: Set[str] = self.options.local_items.value
|
||||
|
||||
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if
|
||||
item in local_items or not item in non_local_items)
|
||||
@@ -247,27 +285,26 @@ class TimespinnerWorld(World):
|
||||
self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells)
|
||||
|
||||
def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None:
|
||||
item_name = self.multiworld.random.choice(item_list)
|
||||
item_name = self.random.choice(item_list)
|
||||
|
||||
self.place_locked_item(excluded_items, location, item_name)
|
||||
|
||||
def place_first_progression_item(self, excluded_items: Set[str]) -> None:
|
||||
if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \
|
||||
or self.precalculated_weights.flood_lake_desolation:
|
||||
if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation:
|
||||
return
|
||||
|
||||
for item in self.multiworld.precollected_items[self.player]:
|
||||
if item.name in starter_progression_items and not item.name in excluded_items:
|
||||
for item_name in self.options.start_inventory.value.keys():
|
||||
if item_name in starter_progression_items:
|
||||
return
|
||||
|
||||
local_starter_progression_items = tuple(
|
||||
item for item in starter_progression_items
|
||||
if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value)
|
||||
if item not in excluded_items and item not in self.options.non_local_items.value)
|
||||
|
||||
if not local_starter_progression_items:
|
||||
return
|
||||
|
||||
progression_item = self.multiworld.random.choice(local_starter_progression_items)
|
||||
progression_item = self.random.choice(local_starter_progression_items)
|
||||
|
||||
self.multiworld.local_early_items[self.player][progression_item] = 1
|
||||
|
||||
@@ -307,9 +344,3 @@ class TimespinnerWorld(World):
|
||||
personal_items[location.address] = location.item.code
|
||||
|
||||
return personal_items
|
||||
|
||||
def is_option_enabled(self, option: str) -> bool:
|
||||
return is_option_enabled(self.multiworld, self.player, option)
|
||||
|
||||
def get_option_value(self, option: str) -> Union[int, Dict, List]:
|
||||
return get_option_value(self.multiworld, self.player, option)
|
||||
|
||||
Reference in New Issue
Block a user