From a02ee4b2b1530ecc9009a96c339c9fff72f74358 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 31 Oct 2023 00:11:02 +0100 Subject: [PATCH 01/15] Implemented new options system into Timespinner --- worlds/timespinner/Locations.py | 20 +-- worlds/timespinner/LogicExtensions.py | 23 +-- worlds/timespinner/Options.py | 94 ++++++------ worlds/timespinner/PreCalculatedWeights.py | 64 ++++----- worlds/timespinner/Regions.py | 21 +-- worlds/timespinner/__init__.py | 159 ++++++++++++--------- 6 files changed, 191 insertions(+), 190 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 70c76b86384..c17c34020e2 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,6 +1,6 @@ from typing import List, Tuple, Optional, Callable, NamedTuple -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic @@ -14,11 +14,11 @@ class LocationData(NamedTuple): rule: Callable[[CollectionState], bool] = lambda state: True -def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: +def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]) -> Tuple[LocationData, ...]: - flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) + flooded: Optional[PreCalculatedWeights] = precalculated_weights + logic = TimespinnerLogic(player, options, precalculated_weights) # 1337000 - 1337155 Generic locations # 1337171 - 1337175 New Pickup checks @@ -203,7 +203,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ] # 1337156 - 1337170 Downloads - if not world or is_option_enabled(world, player, "DownloadableItems"): + if not options or options.downloadable_items: location_table += ( LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), @@ -223,13 +223,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ) # 1337176 - 1337176 Cantoran - if not world or is_option_enabled(world, player, "Cantoran"): + if not options or options.cantoran: location_table += ( LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176), ) # 1337177 - 1337198 Lore Checks - if not world or is_option_enabled(world, player, "LoreChecks"): + if not options or options.lore_checks: location_table += ( LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177), LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178), @@ -258,7 +258,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], # 1337199 - 1337236 Reserved for future use # 1337237 - 1337245 GyreArchives - if not world or is_option_enabled(world, player, "GyreArchives"): + if not options or options.gyre_archives: location_table += ( LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237), LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238), diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index d316a936b02..2774d3bf9b0 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -1,6 +1,6 @@ -from typing import Union -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from typing import Union, Optional +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights @@ -10,17 +10,18 @@ class TimespinnerLogic: flag_unchained_keys: bool flag_eye_spy: bool flag_specific_keycards: bool - pyramid_keys_unlock: Union[str, None] - present_keys_unlock: Union[str, None] - past_keys_unlock: Union[str, None] - time_keys_unlock: Union[str, None] + pyramid_keys_unlock: Optional[str] + present_keys_unlock: Optional[str] + past_keys_unlock: Optional[str] + time_keys_unlock: Optional[str] - def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + def __init__(self, player: int, options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards") - self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy") - self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys") + self.flag_specific_keycards = options and options.Specific_keycards.value + self.flag_eye_spy = options and options.eye_spy.value + self.flag_unchained_keys = options and options.unchained_keys.value if precalculated_weights: if self.flag_unchained_keys: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 8b111849442..87b790a45e0 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,6 +1,6 @@ -from typing import Dict, Union, List -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList +from dataclasses import dataclass +from typing import Dict +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, PerGameCommonOptions from schema import Schema, And, Optional, Or @@ -382,54 +382,40 @@ class Traps(OptionList): valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] - -# Some options that are available in the timespinner randomizer arent currently implemented -timespinner_options: Dict[str, Option] = { - "StartWithJewelryBox": StartWithJewelryBox, - "DownloadableItems": DownloadableItems, - "EyeSpy": EyeSpy, - "StartWithMeyef": StartWithMeyef, - "QuickSeed": QuickSeed, - "SpecificKeycards": SpecificKeycards, - "Inverted": Inverted, - "GyreArchives": GyreArchives, - "Cantoran": Cantoran, - "LoreChecks": LoreChecks, - "BossRando": BossRando, - "BossScaling": BossScaling, - "DamageRando": DamageRando, - "DamageRandoOverrides": DamageRandoOverrides, - "HpCap": HpCap, - "LevelCap": LevelCap, - "ExtraEarringsXP": ExtraEarringsXP, - "BossHealing": BossHealing, - "ShopFill": ShopFill, - "ShopWarpShards": ShopWarpShards, - "ShopMultiplier": ShopMultiplier, - "LootPool": LootPool, - "DropRateCategory": DropRateCategory, - "FixedDropRate": FixedDropRate, - "LootTierDistro": LootTierDistro, - "ShowBestiary": ShowBestiary, - "ShowDrops": ShowDrops, - "EnterSandman": EnterSandman, - "DadPercent": DadPercent, - "RisingTides": RisingTides, - "RisingTidesOverrides": RisingTidesOverrides, - "UnchainedKeys": UnchainedKeys, - "TrapChance": TrapChance, - "Traps": Traps, - "DeathLink": DeathLink, -} - - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: - option = getattr(world, name, None) - if option == None: - return 0 - - return option[player].value +@dataclass +class TimespinnerOptions(PerGameCommonOptions): + start_with_jewelry_box: StartWithJewelryBox + downloadable_items: DownloadableItems + eye_spy: EyeSpy + start_with_meyef: StartWithMeyef + quick_seed: QuickSeed + Specific_keycards: SpecificKeycards + inverted: Inverted + gyre_archives: GyreArchives + cantoran: Cantoran + lore_checks: LoreChecks + boss_rando: BossRando + boss_scaling: BossScaling + damage_rando: DamageRando + damage_rando_overrides: DamageRandoOverrides + hp_cap: HpCap + level_cap: LevelCap + extra_earrings_xp: ExtraEarringsXP + boss_healing: BossHealing + shop_fill: ShopFill + shop_warp_shards: ShopWarpShards + shop_multiplier: ShopMultiplier + loot_pool: LootPool + drop_rate_category: DropRateCategory + fixed_drop_rate: FixedDropRate + loot_tier_distro: LootTierDistro + show_bestiary: ShowBestiary + show_drops: ShowDrops + enter_sandman: EnterSandman + dad_percent: DadPercent + rising_tides: RisingTides + rising_tides_overrides: RisingTidesOverrides + unchained_keys: UnchainedKeys + trap_chance: TrapChance + traps: Traps + death_link: DeathLink diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 64243e25edc..5ca4b2ddc6a 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,6 +1,6 @@ from typing import Tuple, Dict, Union -from BaseClasses import MultiWorld -from .Options import timespinner_options, is_option_enabled, get_option_value +from random import Random +from .Options import TimespinnerOptions class PreCalculatedWeights: pyramid_keys_unlock: str @@ -19,20 +19,20 @@ class PreCalculatedWeights: flood_lake_desolation: bool dry_lake_serene: bool - def __init__(self, world: MultiWorld, player: int): - if world and is_option_enabled(world, player, "RisingTides"): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + def __init__(self, options: TimespinnerOptions, random: Random): + if options.rising_tides: + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options) self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.roll_flood_setting(random, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation") + flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene") self.dry_lake_serene = not flood_lake_serene else: self.flood_basement = False @@ -47,10 +47,12 @@ def __init__(self, world: MultiWorld, player: int): self.dry_lake_serene = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw) + self.get_pyramid_keys_unlocks(options, random, self.flood_maw) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: + def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random, + is_maw_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: Tuple[str, ...] = ( "GateKittyBoss", "GateLeftLibrary", @@ -75,35 +77,27 @@ def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: boo "GateRightPyramid" ) - if not world: - return ( - present_teleportation_gates[0], - present_teleportation_gates[0], - past_teleportation_gates[0], - ancient_pyramid_teleportation_gates[0] - ) - if not is_maw_flooded: past_teleportation_gates += ("GateMaw", ) - if is_option_enabled(world, player, "Inverted"): + if options.inverted: all_gates: Tuple[str, ...] = present_teleportation_gates else: all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates return ( - world.random.choice(all_gates), - world.random.choice(present_teleportation_gates), - world.random.choice(past_teleportation_gates), - world.random.choice(ancient_pyramid_teleportation_gates) + random.choice(all_gates), + random.choice(present_teleportation_gates), + random.choice(past_teleportation_gates), + random.choice(ancient_pyramid_teleportation_gates) ) @staticmethod - def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ - get_option_value(world, player, "RisingTidesOverrides") + options.rising_tides_overrides.value - default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + default_weights: Dict[str, Dict[str, int]] = options.rising_tides_overrides.default if not weights_overrides_option: weights_overrides_option = default_weights @@ -115,13 +109,13 @@ def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Uni return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, - all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: + def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]], + key: str) -> Tuple[bool, bool]: weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] else: result: str = weights diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 905cae867eb..9d4f95c8421 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,13 +1,14 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location -from .Options import is_option_enabled +from .Options import TimespinnerOptions from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): - locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights) +def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions, + precalculated_weights: PreCalculatedWeights): + locationn_datas: Tuple[LocationData] = get_location_datas(player, options, precalculated_weights) locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas) @@ -55,7 +56,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'Space time continuum') ] - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: regions.extend([ create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), @@ -66,10 +67,10 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w world.regions += regions - connectStartingRegion(world, player) + connectStartingRegion(world, player, options) flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) + logic = TimespinnerLogic(player, options, precalculated_weights) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) @@ -181,11 +182,11 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not options.unchained_keys and options.enter_sandman)) connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) @@ -224,12 +225,12 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str return region -def connectStartingRegion(world: MultiWorld, player: int): +def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions): menu = world.get_region('Menu', player) tutorial = world.get_region('Tutorial', player) space_time_continuum = world.get_region('Space time continuum', player) - if is_option_enabled(world, player, "Inverted"): + if options.gyre_archives: starting_region = world.get_region('Refugee Camp', player) else: starting_region = world.get_region('Lake desolation', player) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 24230862bdf..d5f911de15a 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,9 +1,9 @@ -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 TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld @@ -35,8 +35,8 @@ 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 = TimespinnerOptions + options: TimespinnerOptions game = "Timespinner" topology_present = True data_version = 11 @@ -44,24 +44,24 @@ class TimespinnerWorld(World): required_client_version = (0, 3, 7) 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.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 = self.options.start_with_meyef.option_true + if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0: + self.options.quick_seed.value = self.options.quick_seed.option_true + if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: + self.options.start_with_jewelry_box.value = self.options.start_with_jewelry_box.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() @@ -75,7 +75,7 @@ def create_items(self) -> None: 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" @@ -83,46 +83,72 @@ def set_rules(self) -> None: 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"] = self.precalculated_weights.dry_lake_serene - - return slot_data + return { + # options + "StartWithJewelryBox": self.options.start_with_jewelry_box, + "DownloadableItems": self.options.downloadable_items, + "EyeSpy": self.options.eye_spy, + "StartWithMeyef": self.options.start_with_meyef, + "QuickSeed": self.options.quick_seed, + "SpecificKeycards": self.options.Specific_keycards, + "Inverted": self.options.inverted, + "GyreArchives": self.options.gyre_archives, + "Cantoran": self.options.cantoran, + "LoreChecks": self.options.lore_checks, + "BossRando": self.options.boss_rando, + "BossScaling": self.options.boss_scaling, + "DamageRando": self.options.damage_rando, + "DamageRandoOverrides": self.options.damage_rando_overrides, + "HpCap": self.options.hp_cap, + "LevelCap": self.options.level_cap, + "ExtraEarringsXP": self.options.extra_earrings_xp, + "BossHealing": self.options.boss_healing, + "ShopFill": self.options.shop_fill, + "ShopWarpShards": self.options.shop_warp_shards, + "ShopMultiplier": self.options.shop_multiplier, + "LootPool": self.options.loot_pool, + "DropRateCategory": self.options.drop_rate_category, + "FixedDropRate": self.options.fixed_drop_rate, + "LootTierDistro": self.options.loot_tier_distro, + "ShowBestiary": self.options.show_bestiary, + "ShowDrops": self.options.show_drops, + "EnterSandman": self.options.enter_sandman, + "DadPercent": self.options.dad_percent, + "RisingTides": self.options.rising_tides, + "UnchainedKeys": self.options.unchained_keys, + "Traps": self.options.traps, + "DeathLink": self.options.death_link, + "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": self.precalculated_weights.dry_lake_serene, + } 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: @@ -171,41 +197,41 @@ def create_item(self, name: str) -> Item: 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') @@ -244,8 +270,7 @@ def assign_starter_item(self, excluded_items: Set[str], location: str, 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]: @@ -259,7 +284,7 @@ def place_first_progression_item(self, excluded_items: Set[str]) -> None: 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 @@ -298,10 +323,4 @@ def get_personal_items(self) -> Dict[int, int]: if location.address and location.item and location.item.code and location.item.player == self.player: 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) + return personal_items \ No newline at end of file From 0be0739e9b0efc64925b3fc4f35c236afd0b39b7 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 31 Oct 2023 00:15:53 +0100 Subject: [PATCH 02/15] Fixed typo --- worlds/timespinner/LogicExtensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index 2774d3bf9b0..3335245b901 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -19,7 +19,7 @@ def __init__(self, player: int, options: Optional[TimespinnerOptions], precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = options and options.Specific_keycards.value + self.flag_specific_keycards = options and options.specific_keycards.value self.flag_eye_spy = options and options.eye_spy.value self.flag_unchained_keys = options and options.unchained_keys.value From 08baeb745a044f9115a3173d749c33982d02e343 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 31 Oct 2023 00:15:53 +0100 Subject: [PATCH 03/15] Fixed typo --- worlds/timespinner/Options.py | 2 +- worlds/timespinner/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 87b790a45e0..aa845626972 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -389,7 +389,7 @@ class TimespinnerOptions(PerGameCommonOptions): eye_spy: EyeSpy start_with_meyef: StartWithMeyef quick_seed: QuickSeed - Specific_keycards: SpecificKeycards + specific_keycards: SpecificKeycards inverted: Inverted gyre_archives: GyreArchives cantoran: Cantoran diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d5f911de15a..120193cb800 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -90,7 +90,7 @@ def fill_slot_data(self) -> Dict[str, object]: "EyeSpy": self.options.eye_spy, "StartWithMeyef": self.options.start_with_meyef, "QuickSeed": self.options.quick_seed, - "SpecificKeycards": self.options.Specific_keycards, + "SpecificKeycards": self.options.specific_keycards, "Inverted": self.options.inverted, "GyreArchives": self.options.gyre_archives, "Cantoran": self.options.cantoran, From eac14c791ee87a4678dad64b56bf0b45ea01cc3f Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Wed, 1 Nov 2023 20:22:38 +0100 Subject: [PATCH 04/15] Fixed slotdata maybe --- worlds/timespinner/__init__.py | 66 +++++++++++++++++----------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 120193cb800..9fb37c731e3 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -85,39 +85,39 @@ def set_rules(self) -> None: def fill_slot_data(self) -> Dict[str, object]: return { # options - "StartWithJewelryBox": self.options.start_with_jewelry_box, - "DownloadableItems": self.options.downloadable_items, - "EyeSpy": self.options.eye_spy, - "StartWithMeyef": self.options.start_with_meyef, - "QuickSeed": self.options.quick_seed, - "SpecificKeycards": self.options.specific_keycards, - "Inverted": self.options.inverted, - "GyreArchives": self.options.gyre_archives, - "Cantoran": self.options.cantoran, - "LoreChecks": self.options.lore_checks, - "BossRando": self.options.boss_rando, - "BossScaling": self.options.boss_scaling, - "DamageRando": self.options.damage_rando, - "DamageRandoOverrides": self.options.damage_rando_overrides, - "HpCap": self.options.hp_cap, - "LevelCap": self.options.level_cap, - "ExtraEarringsXP": self.options.extra_earrings_xp, - "BossHealing": self.options.boss_healing, - "ShopFill": self.options.shop_fill, - "ShopWarpShards": self.options.shop_warp_shards, - "ShopMultiplier": self.options.shop_multiplier, - "LootPool": self.options.loot_pool, - "DropRateCategory": self.options.drop_rate_category, - "FixedDropRate": self.options.fixed_drop_rate, - "LootTierDistro": self.options.loot_tier_distro, - "ShowBestiary": self.options.show_bestiary, - "ShowDrops": self.options.show_drops, - "EnterSandman": self.options.enter_sandman, - "DadPercent": self.options.dad_percent, - "RisingTides": self.options.rising_tides, - "UnchainedKeys": self.options.unchained_keys, - "Traps": self.options.traps, - "DeathLink": self.options.death_link, + "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, + "BossScaling": self.options.boss_scaling.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, + "Traps": self.options.traps.value, + "DeathLink": self.options.death_link.value, "StinkyMaw": True, # data "PersonalItems": self.get_personal_items(), From 5a2023fbd4e54c55cc3da9033737bc573a464147 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Wed, 22 Nov 2023 23:07:53 +0100 Subject: [PATCH 05/15] Fixes --- worlds/timespinner/Options.py | 1 - worlds/timespinner/Regions.py | 2 +- worlds/timespinner/__init__.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 9792c8b1001..fafb7a595f7 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -414,7 +414,6 @@ class TimespinnerOptions(PerGameCommonOptions): cantoran: Cantoran lore_checks: LoreChecks boss_rando: BossRando - boss_scaling: BossScaling damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides hp_cap: HpCap diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 527e7f53c1e..9a95a007dbd 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -125,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) + connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.present_access_with_wheel_and_spindle and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index d8adb93904b..60321b1d197 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -96,7 +96,6 @@ def fill_slot_data(self) -> Dict[str, object]: "Cantoran": self.options.cantoran.value, "LoreChecks": self.options.lore_checks.value, "BossRando": self.options.boss_rando.value, - "BossScaling": self.options.boss_scaling.value, "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, "HpCap": self.options.hp_cap.value, @@ -116,6 +115,7 @@ def fill_slot_data(self) -> Dict[str, object]: "DadPercent": self.options.dad_percent.value, "RisingTides": self.options.rising_tides.value, "UnchainedKeys": self.options.unchained_keys.value, + "PresentAccessWithWheelAndSpindle": self.options.present_access_with_wheel_and_spindle.value, "Traps": self.options.traps.value, "DeathLink": self.options.death_link.value, "StinkyMaw": True, From 261b45d8b49995f4c34fecfb4d49a813167d38fb Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Wed, 22 Nov 2023 23:26:22 +0100 Subject: [PATCH 06/15] more fixes --- worlds/timespinner/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 60321b1d197..07d1a21fa4b 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -244,15 +244,15 @@ def get_excluded_items(self) -> Set[str]: excluded_items.add('Modern Warp Beacon') excluded_items.add('Mysterious Warp Beacon') - for item in self.multiworld.precollected_items[self.player]: - if item.name not in self.item_name_groups['UseItem']: - excluded_items.add(item.name) + for item_name in self.options.start_inventory.value.keys(): + if item_name not in self.item_name_groups['UseItem']: + excluded_items.add(item_name) 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) @@ -274,7 +274,7 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: 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) @@ -282,8 +282,8 @@ def place_first_progression_item(self, excluded_items: Set[str]) -> None: 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( From 870fd1946005fc163b1523f700854ca6d5ebcafc Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Thu, 23 Nov 2023 13:33:17 +0100 Subject: [PATCH 07/15] Fixed failing unit tests --- worlds/timespinner/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 07d1a21fa4b..9cf48d6e424 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -203,15 +203,16 @@ def create_item(self, name: str) -> Item: if not item.advancement: return item - 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.options.eye_spy: - item.classification = ItemClassification.filler - 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.options.unchained_keys: - item.classification = ItemClassification.filler + if hasattr(self, 'options'): # self.options is not always available in this method + 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.options.eye_spy: + item.classification = ItemClassification.filler + 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.options.unchained_keys: + item.classification = ItemClassification.filler return item From bdc7da9dba781bfa452ad02ee5aa988afc767037 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Thu, 2 May 2024 23:16:38 +0200 Subject: [PATCH 08/15] Implemented options backwards comnpatibility --- worlds/timespinner/Options.py | 230 ++++++++++++++++++++++++++++++++- worlds/timespinner/__init__.py | 10 +- 2 files changed, 232 insertions(+), 8 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index fafb7a595f7..4f4568d323e 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,7 +1,9 @@ from dataclasses import dataclass from typing import Dict -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, PerGameCommonOptions +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility +from Options import PerGameCommonOptions, DeathLinkMixin from schema import Schema, And, Optional, Or +import logging class StartWithJewelryBox(Toggle): @@ -400,9 +402,8 @@ class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" display_name = "Past Wheel & Spindle Warp" - @dataclass -class TimespinnerOptions(PerGameCommonOptions): +class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): start_with_jewelry_box: StartWithJewelryBox downloadable_items: DownloadableItems eye_spy: EyeSpy @@ -437,4 +438,225 @@ class TimespinnerOptions(PerGameCommonOptions): present_access_with_wheel_and_spindle: PresentAccessWithWheelAndSpindle trap_chance: TrapChance traps: Traps - death_link: DeathLink + +@dataclass +class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): + StartWithJewelryBox: StartWithJewelryBox + DownloadableItems: DownloadableItems + EyeSpy: EyeSpy + StartWithMeyef: StartWithMeyef + QuickSeed: QuickSeed + SpecificKeycards: SpecificKeycards + Inverted: Inverted + GyreArchives: GyreArchives + Cantoran: Cantoran + LoreChecks: LoreChecks + BossRando: BossRando + DamageRando: DamageRando + DamageRandoOverrides: DamageRandoOverrides + HpCap: HpCap + LevelCap: LevelCap + ExtraEarringsXP: ExtraEarringsXP + BossHealing: BossHealing + ShopFill: ShopFill + ShopWarpShards: ShopWarpShards + ShopMultiplier: ShopMultiplier + LootPool: LootPool + DropRateCategory: DropRateCategory + FixedDropRate: FixedDropRate + LootTierDistro: LootTierDistro + ShowBestiary: ShowBestiary + ShowDrops: ShowDrops + EnterSandman: EnterSandman + DadPercent: DadPercent + RisingTides: RisingTides + RisingTidesOverrides: RisingTidesOverrides + UnchainedKeys: UnchainedKeys + PresentAccessWithWheelAndSpindle: PresentAccessWithWheelAndSpindle + TrapChance: TrapChance + Traps: Traps + DeathLink: DeathLink + + def __post_init__(self): + self.StartWithJewelryBox.visibility = Visibility.none + self.DownloadableItems.visibility = Visibility.none + self.EyeSpy.visibility = Visibility.none + self.StartWithMeyef.visibility = Visibility.none + self.QuickSeed.visibility = Visibility.none + self.SpecificKeycards.visibility = Visibility.none + self.Inverted.visibility = Visibility.none + self.GyreArchives.visibility = Visibility.none + self.Cantoran.visibility = Visibility.none + self.LoreChecks.visibility = Visibility.none + self.BossRando.visibility = Visibility.none + self.DamageRando.visibility = Visibility.none + self.DamageRandoOverrides.visibility = Visibility.none + self.HpCap.visibility = Visibility.none + self.LevelCap.visibility = Visibility.none + self.ExtraEarringsXP.visibility = Visibility.none + self.BossHealing.visibility = Visibility.none + self.ShopFill.visibility = Visibility.none + self.ShopWarpShards.visibility = Visibility.none + self.ShopMultiplier.visibility = Visibility.none + self.LootPool.visibility = Visibility.none + self.DropRateCategory.visibility = Visibility.none + self.FixedDropRate.visibility = Visibility.none + self.LootTierDistro.visibility = Visibility.none + self.ShowBestiary.visibility = Visibility.none + self.ShowDrops.visibility = Visibility.none + self.EnterSandman.visibility = Visibility.none + self.DadPercent.visibility = Visibility.none + self.RisingTides.visibility = Visibility.none + self.RisingTidesOverrides.visibility = Visibility.none + self.UnchainedKeys.visibility = Visibility.none + self.PresentAccessWithWheelAndSpindle.visibility = Visibility.none + self.TrapChance.visibility = Visibility.none + self.Traps.visibility = Visibility.none + self.DeathLink.visibility = Visibility.none + + def handle_backward_compatibility(o) -> None: + has_replaced_options: bool = False + + if o.StartWithJewelryBox.value != o.StartWithJewelryBox.default and \ + o.start_with_jewelry_box.value == o.start_with_jewelry_box.default: + o.start_with_jewelry_box.value = o.StartWithJewelryBox.value + has_replaced_options = True + if o.DownloadableItems.value != o.DownloadableItems.default and \ + o.downloadable_items.value == o.downloadable_items.default: + o.downloadable_items.value = o.DownloadableItems.value + has_replaced_options = True + if o.EyeSpy.value != o.EyeSpy.default and \ + o.eye_spy.value == o.eye_spy.default: + o.eye_spy.value = o.EyeSpy.value + has_replaced_options = True + if o.StartWithMeyef.value != o.StartWithMeyef.default and \ + o.start_with_meyef.value == o.start_with_meyef.default: + o.start_with_meyef.value = o.StartWithMeyef.value + has_replaced_options = True + if o.QuickSeed.value != o.QuickSeed.default and \ + o.quick_seed.value == o.quick_seed.default: + o.quick_seed.value = o.QuickSeed.value + has_replaced_options = True + if o.SpecificKeycards.value != o.SpecificKeycards.default and \ + o.specific_keycards.value == o.specific_keycards.default: + o.specific_keycards.value = o.SpecificKeycards.value + has_replaced_options = True + if o.Inverted.value != o.Inverted.default and \ + o.inverted.value == o.inverted.default: + o.inverted.value = o.Inverted.value + has_replaced_options = True + if o.GyreArchives.value != o.GyreArchives.default and \ + o.gyre_archives.value == o.gyre_archives.default: + o.gyre_archives.value = o.GyreArchives.value + has_replaced_options = True + if o.Cantoran.value != o.Cantoran.default and \ + o.cantoran.value == o.cantoran.default: + o.cantoran.value = o.Cantoran.value + has_replaced_options = True + if o.LoreChecks.value != o.LoreChecks.default and \ + o.lore_checks.value == o.lore_checks.default: + o.lore_checks.value = o.LoreChecks.value + has_replaced_options = True + if o.BossRando.value != o.BossRando.default and \ + o.boss_rando.value == o.boss_rando.default: + o.boss_rando.value = o.BossRando.value + has_replaced_options = True + if o.DamageRando.value != o.DamageRando.default and \ + o.damage_rando.value == o.damage_rando.default: + o.damage_rando.value = o.DamageRando.value + has_replaced_options = True + if o.DamageRandoOverrides.value != o.DamageRandoOverrides.default and \ + o.damage_rando_overrides.value == o.damage_rando_overrides.default: + o.damage_rando_overrides.value = o.DamageRandoOverrides.value + has_replaced_options = True + if o.HpCap.value != o.HpCap.default and \ + o.hp_cap.value == o.hp_cap.default: + o.hp_cap.value = o.HpCap.value + has_replaced_options = True + if o.LevelCap.value != o.LevelCap.default and \ + o.level_cap.value == o.level_cap.default: + o.level_cap.value = o.LevelCap.value + has_replaced_options = True + if o.ExtraEarringsXP.value != o.ExtraEarringsXP.default and \ + o.extra_earrings_xp.value == o.extra_earrings_xp.default: + o.extra_earrings_xp.value = o.ExtraEarringsXP.value + has_replaced_options = True + if o.BossHealing.value != o.BossHealing.default and \ + o.boss_healing.value == o.boss_healing.default: + o.boss_healing.value = o.BossHealing.value + has_replaced_options = True + if o.ShopFill.value != o.ShopFill.default and \ + o.shop_fill.value == o.shop_fill.default: + o.shop_fill.value = o.ShopFill.value + has_replaced_options = True + if o.ShopWarpShards.value != o.ShopWarpShards.default and \ + o.shop_warp_shards.value == o.shop_warp_shards.default: + o.shop_warp_shards.value = o.ShopWarpShards.value + has_replaced_options = True + if o.ShopMultiplier.value != o.ShopMultiplier.default and \ + o.shop_multiplier.value == o.shop_multiplier.default: + o.shop_multiplier.value = o.ShopMultiplier.value + has_replaced_options = True + if o.LootPool.value != o.LootPool.default and \ + o.loot_pool.value == o.loot_pool.default: + o.loot_pool.value = o.LootPool.value + has_replaced_options = True + if o.DropRateCategory.value != o.DropRateCategory.default and \ + o.drop_rate_category.value == o.drop_rate_category.default: + o.drop_rate_category.value = o.DropRateCategory.value + has_replaced_options = True + if o.FixedDropRate.value != o.FixedDropRate.default and \ + o.fixed_drop_rate.value == o.fixed_drop_rate.default: + o.fixed_drop_rate.value = o.FixedDropRate.value + has_replaced_options = True + if o.LootTierDistro.value != o.LootTierDistro.default and \ + o.loot_tier_distro.value == o.loot_tier_distro.default: + o.loot_tier_distro.value = o.LootTierDistro.value + has_replaced_options = True + if o.ShowBestiary.value != o.ShowBestiary.default and \ + o.show_bestiary.value == o.show_bestiary.default: + o.show_bestiary.value = o.ShowBestiary.value + has_replaced_options = True + if o.ShowDrops.value != o.ShowDrops.default and \ + o.show_drops.value == o.show_drops.default: + o.show_drops.value = o.ShowDrops.value + has_replaced_options = True + if o.EnterSandman.value != o.EnterSandman.default and \ + o.enter_sandman.value == o.enter_sandman.default: + o.enter_sandman.value = o.EnterSandman.value + has_replaced_options = True + if o.DadPercent.value != o.DadPercent.default and \ + o.dad_percent.value == o.dad_percent.default: + o.dad_percent.value = o.DadPercent.value + has_replaced_options = True + if o.RisingTides.value != o.RisingTides.default and \ + o.rising_tides.value == o.rising_tides.default: + o.rising_tides.value = o.RisingTides.value + has_replaced_options = True + if o.RisingTidesOverrides.value != o.RisingTidesOverrides.default and \ + o.rising_tides_overrides.value == o.rising_tides_overrides.default: + o.rising_tides_overrides.value = o.RisingTidesOverrides.value + has_replaced_options = True + if o.UnchainedKeys.value != o.UnchainedKeys.default and \ + o.unchained_keys.value == o.unchained_keys.default: + o.unchained_keys.value = o.UnchainedKeys.value + has_replaced_options = True + if o.PresentAccessWithWheelAndSpindle.value != o.PresentAccessWithWheelAndSpindle.default and \ + o.present_access_with_wheel_and_spindle.value == o.present_access_with_wheel_and_spindle.default: + o.present_access_with_wheel_and_spindle.value = o.PresentAccessWithWheelAndSpindle.value + has_replaced_options = True + if o.TrapChance.value != o.TrapChance.default and \ + o.trap_chance.value == o.trap_chance.default: + o.trap_chance.value = o.TrapChance.value + has_replaced_options = True + if o.Traps.value != o.Traps.default and \ + o.traps.value == o.traps.default: + o.traps.value = o.Traps.value + has_replaced_options = True + if o.DeathLink.value != o.DeathLink.default and \ + o.death_link.value == o.death_link.default: + o.death_link.value = o.DeathLink.value + has_replaced_options = True + + if has_replaced_options: + logging.warning("Timespinner options where renamed from PasCalCase to snake_case, plz update your yml") \ No newline at end of file diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 9cf48d6e424..0d4a7d3ccc0 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -3,7 +3,7 @@ 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 TimespinnerOptions +from .Options import BackwardsCompatiableTimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld @@ -35,8 +35,8 @@ 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. """ - options_dataclass = TimespinnerOptions - options: TimespinnerOptions + options_dataclass = BackwardsCompatiableTimespinnerOptions + options: BackwardsCompatiableTimespinnerOptions game = "Timespinner" topology_present = True data_version = 12 @@ -60,6 +60,8 @@ def generate_early(self) -> None: if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: self.options.start_with_jewelry_box.value = self.options.start_with_jewelry_box.option_true + self.options.handle_backward_compatibility() + def create_regions(self) -> None: create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) @@ -333,4 +335,4 @@ def get_personal_items(self) -> Dict[int, int]: if location.address and location.item and location.item.code and location.item.player == self.player: personal_items[location.address] = location.item.code - return personal_items \ No newline at end of file + return personal_items From cae983c44b049b119059eecfc6516004bddf5b4e Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Fri, 3 May 2024 19:11:37 +0200 Subject: [PATCH 09/15] Fixed option fallbacks --- worlds/timespinner/Options.py | 169 +++++++++++---------------------- worlds/timespinner/Regions.py | 2 +- worlds/timespinner/__init__.py | 2 +- 3 files changed, 57 insertions(+), 116 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 4f4568d323e..76b13d905c1 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,61 +1,51 @@ from dataclasses import dataclass +from typing import Type, Any from typing import Dict -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility -from Options import PerGameCommonOptions, DeathLinkMixin +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option +from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions from schema import Schema, And, Optional, Or import logging - class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" display_name = "Start with Jewelry Box" - class DownloadableItems(DefaultOnToggle): "With the tablet you will be able to download items at terminals" display_name = "Downloadable items" - class EyeSpy(Toggle): "Requires Oculus Ring in inventory to be able to break hidden walls." display_name = "Eye Spy" - class StartWithMeyef(Toggle): "Start with Meyef, ideal for when you want to play multiplayer." display_name = "Start with Meyef" - class QuickSeed(Toggle): "Start with Talaria Attachment, Nyoom!" display_name = "Quick seed" - class SpecificKeycards(Toggle): "Keycards can only open corresponding doors" display_name = "Specific Keycards" - class Inverted(Toggle): "Start in the past" display_name = "Inverted" - class GyreArchives(Toggle): "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" display_name = "Gyre Archives" - class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" - class LoreChecks(Toggle): "Memories and journal entries contain items." display_name = "Lore Checks" - class BossRando(Choice): "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" @@ -64,7 +54,6 @@ class BossRando(Choice): option_unscaled = 2 alias_true = 1 - class EnemyRando(Choice): "Wheter enemies will be randomized, and if their damage/hp should be scaled." display_name = "Enemy Randomization" @@ -74,7 +63,6 @@ class EnemyRando(Choice): option_ryshia = 3 alias_true = 1 - class DamageRando(Choice): "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." display_name = "Damage Rando" @@ -87,7 +75,6 @@ class DamageRando(Choice): option_manual = 6 alias_true = 2 - class DamageRandoOverrides(OptionDict): """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds""" @@ -193,7 +180,6 @@ class DamageRandoOverrides(OptionDict): "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, } - class HpCap(Range): "Sets the number that Lunais's HP maxes out at." display_name = "HP Cap" @@ -201,7 +187,6 @@ class HpCap(Range): range_end = 999 default = 999 - class LevelCap(Range): """Sets the max level Lunais can achieve.""" display_name = "Level Cap" @@ -209,20 +194,17 @@ class LevelCap(Range): range_end = 99 default = 99 - class ExtraEarringsXP(Range): """Adds additional XP granted by Galaxy Earrings.""" display_name = "Extra Earrings XP" range_start = 0 range_end = 24 default = 0 - class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" - class ShopFill(Choice): """Sets the items for sale in Merchant Crow's shops. Default: No sunglasses or trendy jacket, but sand vials for sale. @@ -235,12 +217,10 @@ class ShopFill(Choice): option_vanilla = 2 option_empty = 3 - class ShopWarpShards(DefaultOnToggle): "Shops always sell warp shards (when keys possessed), ignoring inventory setting." display_name = "Always Sell Warp Shards" - class ShopMultiplier(Range): "Multiplier for the cost of items in the shop. Set to 0 for free shops." display_name = "Shop Price Multiplier" @@ -248,7 +228,6 @@ class ShopMultiplier(Range): range_end = 10 default = 1 - class LootPool(Choice): """Sets the items that drop from enemies (does not apply to boss reward checks) Vanilla: Drops are the same as the base game @@ -259,7 +238,6 @@ class LootPool(Choice): option_randomized = 1 option_empty = 2 - class DropRateCategory(Choice): """Sets the drop rate when 'Loot Pool' is set to 'Random' Tiered: Based on item rarity/value @@ -273,7 +251,6 @@ class DropRateCategory(Choice): option_randomized = 2 option_fixed = 3 - class FixedDropRate(Range): "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" display_name = "Fixed Drop Rate" @@ -281,7 +258,6 @@ class FixedDropRate(Range): range_end = 100 default = 5 - class LootTierDistro(Choice): """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items @@ -293,32 +269,26 @@ class LootTierDistro(Choice): option_full_random = 1 option_inverted_weight = 2 - class ShowBestiary(Toggle): "All entries in the bestiary are visible, without needing to kill one of a given enemy first" display_name = "Show Bestiary Entries" - class ShowDrops(Toggle): "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" display_name = "Show Bestiary Item Drops" - class EnterSandman(Toggle): "The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces" display_name = "Enter Sandman" - class DadPercent(Toggle): """The win condition is beating the boss of Emperor's Tower""" display_name = "Dad Percent" - class RisingTides(Toggle): """Random areas are flooded or drained, can be further specified with RisingTidesOverrides""" display_name = "Rising Tides" - def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]: if with_save_point_option: return { @@ -343,7 +313,6 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D "Flooded") } - class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" @@ -375,13 +344,11 @@ class RisingTidesOverrides(OptionDict): "Lab": { "Dry": 67, "Flooded": 33 }, } - class UnchainedKeys(Toggle): """Start with Twin Pyramid Key, which does not give free warp; warp items for Past, Present, (and ??? with Enter Sandman) can be found.""" display_name = "Unchained Keys" - class TrapChance(Range): """Chance of traps in the item pool. Traps will only replace filler items such as potions, vials and antidotes""" @@ -390,17 +357,15 @@ class TrapChance(Range): range_end = 100 default = 10 - class Traps(OptionList): """List of traps that may be in the item pool to find""" display_name = "Traps Types" valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] - class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" - display_name = "Past Wheel & Spindle Warp" + display_name = "Back to the future" @dataclass class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): @@ -435,84 +400,60 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): rising_tides: RisingTides rising_tides_overrides: RisingTidesOverrides unchained_keys: UnchainedKeys - present_access_with_wheel_and_spindle: PresentAccessWithWheelAndSpindle + back_to_the_future: PresentAccessWithWheelAndSpindle trap_chance: TrapChance traps: Traps +class HiddenDamageRandoOverrides(DamageRandoOverrides): + visibility = Visibility.none + +class HiddenRisingTidesOverrides(RisingTidesOverrides): + visibility = Visibility.none + @dataclass class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): - StartWithJewelryBox: StartWithJewelryBox - DownloadableItems: DownloadableItems - EyeSpy: EyeSpy - StartWithMeyef: StartWithMeyef - QuickSeed: QuickSeed - SpecificKeycards: SpecificKeycards - Inverted: Inverted - GyreArchives: GyreArchives - Cantoran: Cantoran - LoreChecks: LoreChecks - BossRando: BossRando - DamageRando: DamageRando - DamageRandoOverrides: DamageRandoOverrides - HpCap: HpCap - LevelCap: LevelCap - ExtraEarringsXP: ExtraEarringsXP - BossHealing: BossHealing - ShopFill: ShopFill - ShopWarpShards: ShopWarpShards - ShopMultiplier: ShopMultiplier - LootPool: LootPool - DropRateCategory: DropRateCategory - FixedDropRate: FixedDropRate - LootTierDistro: LootTierDistro - ShowBestiary: ShowBestiary - ShowDrops: ShowDrops - EnterSandman: EnterSandman - DadPercent: DadPercent - RisingTides: RisingTides - RisingTidesOverrides: RisingTidesOverrides - UnchainedKeys: UnchainedKeys - PresentAccessWithWheelAndSpindle: PresentAccessWithWheelAndSpindle - TrapChance: TrapChance - Traps: Traps - DeathLink: DeathLink - - def __post_init__(self): - self.StartWithJewelryBox.visibility = Visibility.none - self.DownloadableItems.visibility = Visibility.none - self.EyeSpy.visibility = Visibility.none - self.StartWithMeyef.visibility = Visibility.none - self.QuickSeed.visibility = Visibility.none - self.SpecificKeycards.visibility = Visibility.none - self.Inverted.visibility = Visibility.none - self.GyreArchives.visibility = Visibility.none - self.Cantoran.visibility = Visibility.none - self.LoreChecks.visibility = Visibility.none - self.BossRando.visibility = Visibility.none - self.DamageRando.visibility = Visibility.none - self.DamageRandoOverrides.visibility = Visibility.none - self.HpCap.visibility = Visibility.none - self.LevelCap.visibility = Visibility.none - self.ExtraEarringsXP.visibility = Visibility.none - self.BossHealing.visibility = Visibility.none - self.ShopFill.visibility = Visibility.none - self.ShopWarpShards.visibility = Visibility.none - self.ShopMultiplier.visibility = Visibility.none - self.LootPool.visibility = Visibility.none - self.DropRateCategory.visibility = Visibility.none - self.FixedDropRate.visibility = Visibility.none - self.LootTierDistro.visibility = Visibility.none - self.ShowBestiary.visibility = Visibility.none - self.ShowDrops.visibility = Visibility.none - self.EnterSandman.visibility = Visibility.none - self.DadPercent.visibility = Visibility.none - self.RisingTides.visibility = Visibility.none - self.RisingTidesOverrides.visibility = Visibility.none - self.UnchainedKeys.visibility = Visibility.none - self.PresentAccessWithWheelAndSpindle.visibility = Visibility.none - self.TrapChance.visibility = Visibility.none - self.Traps.visibility = Visibility.none - self.DeathLink.visibility = Visibility.none + + @staticmethod + def hidden(option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + return new_option + + StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: hidden(DownloadableItems) # type: ignore + EyeSpy: hidden(EyeSpy) # type: ignore + StartWithMeyef: hidden(StartWithMeyef) # type: ignore + QuickSeed: hidden(QuickSeed) # type: ignore + SpecificKeycards: hidden(SpecificKeycards) # type: ignore + Inverted: hidden(Inverted) # type: ignore + GyreArchives: hidden(GyreArchives) # type: ignore + Cantoran: hidden(Cantoran) # type: ignore + LoreChecks: hidden(LoreChecks) # type: ignore + BossRando: hidden(BossRando) # type: ignore + DamageRando: hidden(DamageRando) # type: ignore + DamageRandoOverrides: HiddenDamageRandoOverrides + HpCap: hidden(HpCap) # type: ignore + LevelCap: hidden(LevelCap) # type: ignore + ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore + BossHealing: hidden(BossHealing) # type: ignore + ShopFill: hidden(ShopFill) # type: ignore + ShopWarpShards: hidden(ShopWarpShards) # type: ignore + ShopMultiplier: hidden(ShopMultiplier) # type: ignore + LootPool: hidden(LootPool) # type: ignore + DropRateCategory: hidden(DropRateCategory) # type: ignore + FixedDropRate: hidden(FixedDropRate) # type: ignore + LootTierDistro: hidden(LootTierDistro) # type: ignore + ShowBestiary: hidden(ShowBestiary) # type: ignore + ShowDrops: hidden(ShowDrops) # type: ignore + EnterSandman: hidden(EnterSandman) # type: ignore + DadPercent: hidden(DadPercent) # type: ignore + RisingTides: hidden(RisingTides) # type: ignore + RisingTidesOverrides: HiddenRisingTidesOverrides + UnchainedKeys: hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: hidden(TrapChance) # type: ignore + Traps: hidden(Traps) # type: ignore + DeathLink: hidden(DeathLink) # type: ignore def handle_backward_compatibility(o) -> None: has_replaced_options: bool = False @@ -642,8 +583,8 @@ def handle_backward_compatibility(o) -> None: o.unchained_keys.value = o.UnchainedKeys.value has_replaced_options = True if o.PresentAccessWithWheelAndSpindle.value != o.PresentAccessWithWheelAndSpindle.default and \ - o.present_access_with_wheel_and_spindle.value == o.present_access_with_wheel_and_spindle.default: - o.present_access_with_wheel_and_spindle.value = o.PresentAccessWithWheelAndSpindle.value + o.back_to_the_future.value == o.back_to_the_future.default: + o.back_to_the_future.value = o.PresentAccessWithWheelAndSpindle.value has_replaced_options = True if o.TrapChance.value != o.TrapChance.default and \ o.trap_chance.value == o.trap_chance.default: diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index a1a8166b6bc..2a972fdb0c8 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -125,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.present_access_with_wheel_and_spindle and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) + connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.back_to_the_future and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 0d4a7d3ccc0..d688318b3bb 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -117,7 +117,7 @@ def fill_slot_data(self) -> Dict[str, object]: "DadPercent": self.options.dad_percent.value, "RisingTides": self.options.rising_tides.value, "UnchainedKeys": self.options.unchained_keys.value, - "PresentAccessWithWheelAndSpindle": self.options.present_access_with_wheel_and_spindle.value, + "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, "Traps": self.options.traps.value, "DeathLink": self.options.death_link.value, "StinkyMaw": True, From bffebb1ad8b4c4b4043ca139f7fe6da82e742120 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 1 Jun 2024 19:14:49 +0200 Subject: [PATCH 10/15] Implemented review results --- worlds/timespinner/Options.py | 212 ++++++++++++++++----------------- worlds/timespinner/__init__.py | 27 ++--- 2 files changed, 119 insertions(+), 120 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 76b13d905c1..cced590154a 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -455,148 +455,148 @@ def hidden(option: Type[Option[Any]]) -> Type[Option]: Traps: hidden(Traps) # type: ignore DeathLink: hidden(DeathLink) # type: ignore - def handle_backward_compatibility(o) -> None: + def handle_backward_compatibility(self) -> None: has_replaced_options: bool = False - if o.StartWithJewelryBox.value != o.StartWithJewelryBox.default and \ - o.start_with_jewelry_box.value == o.start_with_jewelry_box.default: - o.start_with_jewelry_box.value = o.StartWithJewelryBox.value + if self.StartWithJewelryBox != StartWithJewelryBox.default and \ + self.start_with_jewelry_box == StartWithJewelryBox.default: + self.start_with_jewelry_box.value = self.StartWithJewelryBox.value has_replaced_options = True - if o.DownloadableItems.value != o.DownloadableItems.default and \ - o.downloadable_items.value == o.downloadable_items.default: - o.downloadable_items.value = o.DownloadableItems.value + if self.DownloadableItems != DownloadableItems.default and \ + self.downloadable_items == DownloadableItems.default: + self.downloadable_items.value = self.DownloadableItems.value has_replaced_options = True - if o.EyeSpy.value != o.EyeSpy.default and \ - o.eye_spy.value == o.eye_spy.default: - o.eye_spy.value = o.EyeSpy.value + if self.EyeSpy != EyeSpy.default and \ + self.eye_spy == EyeSpy.default: + self.eye_spy.value = self.EyeSpy.value has_replaced_options = True - if o.StartWithMeyef.value != o.StartWithMeyef.default and \ - o.start_with_meyef.value == o.start_with_meyef.default: - o.start_with_meyef.value = o.StartWithMeyef.value + if self.StartWithMeyef != StartWithMeyef.default and \ + self.start_with_meyef == StartWithMeyef.default: + self.start_with_meyef.value = self.StartWithMeyef.value has_replaced_options = True - if o.QuickSeed.value != o.QuickSeed.default and \ - o.quick_seed.value == o.quick_seed.default: - o.quick_seed.value = o.QuickSeed.value + if self.QuickSeed != QuickSeed.default and \ + self.quick_seed == QuickSeed.default: + self.quick_seed.value = self.QuickSeed.value has_replaced_options = True - if o.SpecificKeycards.value != o.SpecificKeycards.default and \ - o.specific_keycards.value == o.specific_keycards.default: - o.specific_keycards.value = o.SpecificKeycards.value + if self.SpecificKeycards != SpecificKeycards.default and \ + self.specific_keycards == SpecificKeycards.default: + self.specific_keycards.value = self.SpecificKeycards.value has_replaced_options = True - if o.Inverted.value != o.Inverted.default and \ - o.inverted.value == o.inverted.default: - o.inverted.value = o.Inverted.value + if self.Inverted != Inverted.default and \ + self.inverted == Inverted.default: + self.inverted.value = self.Inverted.value has_replaced_options = True - if o.GyreArchives.value != o.GyreArchives.default and \ - o.gyre_archives.value == o.gyre_archives.default: - o.gyre_archives.value = o.GyreArchives.value + if self.GyreArchives != GyreArchives.default and \ + self.gyre_archives == GyreArchives.default: + self.gyre_archives.value = self.GyreArchives.value has_replaced_options = True - if o.Cantoran.value != o.Cantoran.default and \ - o.cantoran.value == o.cantoran.default: - o.cantoran.value = o.Cantoran.value + if self.Cantoran != Cantoran.default and \ + self.cantoran == Cantoran.default: + self.cantoran.value = self.Cantoran.value has_replaced_options = True - if o.LoreChecks.value != o.LoreChecks.default and \ - o.lore_checks.value == o.lore_checks.default: - o.lore_checks.value = o.LoreChecks.value + if self.LoreChecks != LoreChecks.default and \ + self.lore_checks == LoreChecks.default: + self.lore_checks.value = self.LoreChecks.value has_replaced_options = True - if o.BossRando.value != o.BossRando.default and \ - o.boss_rando.value == o.boss_rando.default: - o.boss_rando.value = o.BossRando.value + if self.BossRando != BossRando.default and \ + self.boss_rando == BossRando.default: + self.boss_rando.value = self.BossRando.value has_replaced_options = True - if o.DamageRando.value != o.DamageRando.default and \ - o.damage_rando.value == o.damage_rando.default: - o.damage_rando.value = o.DamageRando.value + if self.DamageRando != DamageRando.default and \ + self.damage_rando == DamageRando.default: + self.damage_rando.value = self.DamageRando.value has_replaced_options = True - if o.DamageRandoOverrides.value != o.DamageRandoOverrides.default and \ - o.damage_rando_overrides.value == o.damage_rando_overrides.default: - o.damage_rando_overrides.value = o.DamageRandoOverrides.value + if self.DamageRandoOverrides != DamageRandoOverrides.default and \ + self.damage_rando_overrides == DamageRandoOverrides.default: + self.damage_rando_overrides.value = self.DamageRandoOverrides.value has_replaced_options = True - if o.HpCap.value != o.HpCap.default and \ - o.hp_cap.value == o.hp_cap.default: - o.hp_cap.value = o.HpCap.value + if self.HpCap != HpCap.default and \ + self.hp_cap == HpCap.default: + self.hp_cap.value = self.HpCap.value has_replaced_options = True - if o.LevelCap.value != o.LevelCap.default and \ - o.level_cap.value == o.level_cap.default: - o.level_cap.value = o.LevelCap.value + if self.LevelCap != LevelCap.default and \ + self.level_cap == LevelCap.default: + self.level_cap.value = self.LevelCap.value has_replaced_options = True - if o.ExtraEarringsXP.value != o.ExtraEarringsXP.default and \ - o.extra_earrings_xp.value == o.extra_earrings_xp.default: - o.extra_earrings_xp.value = o.ExtraEarringsXP.value + if self.ExtraEarringsXP != ExtraEarringsXP.default and \ + self.extra_earrings_xp == ExtraEarringsXP.default: + self.extra_earrings_xp.value = self.ExtraEarringsXP.value has_replaced_options = True - if o.BossHealing.value != o.BossHealing.default and \ - o.boss_healing.value == o.boss_healing.default: - o.boss_healing.value = o.BossHealing.value + if self.BossHealing != BossHealing.default and \ + self.boss_healing == BossHealing.default: + self.boss_healing.value = self.BossHealing.value has_replaced_options = True - if o.ShopFill.value != o.ShopFill.default and \ - o.shop_fill.value == o.shop_fill.default: - o.shop_fill.value = o.ShopFill.value + if self.ShopFill != ShopFill.default and \ + self.shop_fill == ShopFill.default: + self.shop_fill.value = self.ShopFill.value has_replaced_options = True - if o.ShopWarpShards.value != o.ShopWarpShards.default and \ - o.shop_warp_shards.value == o.shop_warp_shards.default: - o.shop_warp_shards.value = o.ShopWarpShards.value + if self.ShopWarpShards != ShopWarpShards.default and \ + self.shop_warp_shards == ShopWarpShards.default: + self.shop_warp_shards.value = self.ShopWarpShards.value has_replaced_options = True - if o.ShopMultiplier.value != o.ShopMultiplier.default and \ - o.shop_multiplier.value == o.shop_multiplier.default: - o.shop_multiplier.value = o.ShopMultiplier.value + if self.ShopMultiplier != ShopMultiplier.default and \ + self.shop_multiplier == ShopMultiplier.default: + self.shop_multiplier.value = self.ShopMultiplier.value has_replaced_options = True - if o.LootPool.value != o.LootPool.default and \ - o.loot_pool.value == o.loot_pool.default: - o.loot_pool.value = o.LootPool.value + if self.LootPool != LootPool.default and \ + self.loot_pool == LootPool.default: + self.loot_pool.value = self.LootPool.value has_replaced_options = True - if o.DropRateCategory.value != o.DropRateCategory.default and \ - o.drop_rate_category.value == o.drop_rate_category.default: - o.drop_rate_category.value = o.DropRateCategory.value + if self.DropRateCategory != DropRateCategory.default and \ + self.drop_rate_category == DropRateCategory.default: + self.drop_rate_category.value = self.DropRateCategory.value has_replaced_options = True - if o.FixedDropRate.value != o.FixedDropRate.default and \ - o.fixed_drop_rate.value == o.fixed_drop_rate.default: - o.fixed_drop_rate.value = o.FixedDropRate.value + if self.FixedDropRate != FixedDropRate.default and \ + self.fixed_drop_rate == FixedDropRate.default: + self.fixed_drop_rate.value = self.FixedDropRate.value has_replaced_options = True - if o.LootTierDistro.value != o.LootTierDistro.default and \ - o.loot_tier_distro.value == o.loot_tier_distro.default: - o.loot_tier_distro.value = o.LootTierDistro.value + if self.LootTierDistro != LootTierDistro.default and \ + self.loot_tier_distro == LootTierDistro.default: + self.loot_tier_distro.value = self.LootTierDistro.value has_replaced_options = True - if o.ShowBestiary.value != o.ShowBestiary.default and \ - o.show_bestiary.value == o.show_bestiary.default: - o.show_bestiary.value = o.ShowBestiary.value + if self.ShowBestiary != ShowBestiary.default and \ + self.show_bestiary == ShowBestiary.default: + self.show_bestiary.value = self.ShowBestiary.value has_replaced_options = True - if o.ShowDrops.value != o.ShowDrops.default and \ - o.show_drops.value == o.show_drops.default: - o.show_drops.value = o.ShowDrops.value + if self.ShowDrops != ShowDrops.default and \ + self.show_drops == ShowDrops.default: + self.show_drops.value = self.ShowDrops.value has_replaced_options = True - if o.EnterSandman.value != o.EnterSandman.default and \ - o.enter_sandman.value == o.enter_sandman.default: - o.enter_sandman.value = o.EnterSandman.value + if self.EnterSandman != EnterSandman.default and \ + self.enter_sandman == EnterSandman.default: + self.enter_sandman.value = self.EnterSandman.value has_replaced_options = True - if o.DadPercent.value != o.DadPercent.default and \ - o.dad_percent.value == o.dad_percent.default: - o.dad_percent.value = o.DadPercent.value + if self.DadPercent != DadPercent.default and \ + self.dad_percent == DadPercent.default: + self.dad_percent.value = self.DadPercent.value has_replaced_options = True - if o.RisingTides.value != o.RisingTides.default and \ - o.rising_tides.value == o.rising_tides.default: - o.rising_tides.value = o.RisingTides.value + if self.RisingTides != RisingTides.default and \ + self.rising_tides == RisingTides.default: + self.rising_tides.value = self.RisingTides.value has_replaced_options = True - if o.RisingTidesOverrides.value != o.RisingTidesOverrides.default and \ - o.rising_tides_overrides.value == o.rising_tides_overrides.default: - o.rising_tides_overrides.value = o.RisingTidesOverrides.value + if self.RisingTidesOverrides != RisingTidesOverrides.default and \ + self.rising_tides_overrides == RisingTidesOverrides.default: + self.rising_tides_overrides.value = self.RisingTidesOverrides.value has_replaced_options = True - if o.UnchainedKeys.value != o.UnchainedKeys.default and \ - o.unchained_keys.value == o.unchained_keys.default: - o.unchained_keys.value = o.UnchainedKeys.value + if self.UnchainedKeys != UnchainedKeys.default and \ + self.unchained_keys == UnchainedKeys.default: + self.unchained_keys.value = self.UnchainedKeys.value has_replaced_options = True - if o.PresentAccessWithWheelAndSpindle.value != o.PresentAccessWithWheelAndSpindle.default and \ - o.back_to_the_future.value == o.back_to_the_future.default: - o.back_to_the_future.value = o.PresentAccessWithWheelAndSpindle.value + if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \ + self.back_to_the_future == PresentAccessWithWheelAndSpindle.default: + self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value has_replaced_options = True - if o.TrapChance.value != o.TrapChance.default and \ - o.trap_chance.value == o.trap_chance.default: - o.trap_chance.value = o.TrapChance.value + if self.TrapChance != TrapChance.default and \ + self.trap_chance == TrapChance.default: + self.trap_chance.value = self.TrapChance.value has_replaced_options = True - if o.Traps.value != o.Traps.default and \ - o.traps.value == o.traps.default: - o.traps.value = o.Traps.value + if self.Traps != Traps.default and \ + self.traps == Traps.default: + self.traps.value = self.Traps.value has_replaced_options = True - if o.DeathLink.value != o.DeathLink.default and \ - o.death_link.value == o.death_link.default: - o.death_link.value = o.DeathLink.value + if self.DeathLink != DeathLink.default and \ + self.death_link == DeathLink.default: + self.death_link.value = self.DeathLink.value has_replaced_options = True if has_replaced_options: diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 3123cbcaffd..b3fa90056d4 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -3,7 +3,7 @@ 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 BackwardsCompatiableTimespinnerOptions +from .Options import BackwardsCompatiableTimespinnerOptions, Toggle from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld @@ -53,11 +53,11 @@ def generate_early(self) -> None: # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly if self.options.start_inventory.value.pop('Meyef', 0) > 0: - self.options.start_with_meyef.value = self.options.start_with_meyef.option_true + 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 = self.options.quick_seed.option_true + 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 = self.options.start_with_jewelry_box.option_true + self.options.start_with_jewelry_box.value = Toggle.option_true self.options.handle_backward_compatibility() @@ -204,16 +204,15 @@ def create_item(self, name: str) -> Item: if not item.advancement: return item - if hasattr(self, 'options'): # self.options is not always available in this method - 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.options.eye_spy: - item.classification = ItemClassification.filler - 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.options.unchained_keys: - item.classification = ItemClassification.filler + 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.options.eye_spy: + item.classification = ItemClassification.filler + 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.options.unchained_keys: + item.classification = ItemClassification.filler return item From d6ea280d1b714ec77d85f65a34d95a13678114b3 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 2 Jun 2024 18:03:04 +0200 Subject: [PATCH 11/15] Fixed logic bug --- worlds/timespinner/Options.py | 11 ++++++++++- worlds/timespinner/Regions.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index cced590154a..587ba6ff300 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -405,9 +405,17 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): traps: Traps class HiddenDamageRandoOverrides(DamageRandoOverrides): + """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that + you don't specify will roll with 1/1/1 as odds""" visibility = Visibility.none class HiddenRisingTidesOverrides(RisingTidesOverrides): + """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. + Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + visibility = Visibility.none + +class HiddenTraps(Traps): + """List of traps that may be in the item pool to find""" visibility = Visibility.none @dataclass @@ -417,6 +425,7 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): def hidden(option: Type[Option[Any]]) -> Type[Option]: new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ return new_option StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore @@ -452,7 +461,7 @@ def hidden(option: Type[Option[Any]]) -> Type[Option]: UnchainedKeys: hidden(UnchainedKeys) # type: ignore PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore TrapChance: hidden(TrapChance) # type: ignore - Traps: hidden(Traps) # type: ignore + Traps: HiddenTraps # type: ignore DeathLink: hidden(DeathLink) # type: ignore def handle_backward_compatibility(self) -> None: diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 2a972fdb0c8..5d2d82254e4 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -227,7 +227,7 @@ def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOp tutorial = world.get_region('Tutorial', player) space_time_continuum = world.get_region('Space time continuum', player) - if options.gyre_archives: + if options.inverted: starting_region = world.get_region('Refugee Camp', player) else: starting_region = world.get_region('Lake desolation', player) From cb30193a358312a5feb53e5b62c51709896956a3 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 2 Jun 2024 19:14:45 +0200 Subject: [PATCH 12/15] Fixed python 3.8/3.9 compatibility --- worlds/timespinner/Options.py | 74 +++++++++++++++++------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 587ba6ff300..5bbf20fc0b2 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -418,51 +418,51 @@ class HiddenTraps(Traps): """List of traps that may be in the item pool to find""" visibility = Visibility.none -@dataclass -class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): - - @staticmethod - def hidden(option: Type[Option[Any]]) -> Type[Option]: +class OptionsHider: + @classmethod + def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) new_option.visibility = Visibility.none new_option.__doc__ = option.__doc__ return new_option - StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore - DownloadableItems: hidden(DownloadableItems) # type: ignore - EyeSpy: hidden(EyeSpy) # type: ignore - StartWithMeyef: hidden(StartWithMeyef) # type: ignore - QuickSeed: hidden(QuickSeed) # type: ignore - SpecificKeycards: hidden(SpecificKeycards) # type: ignore - Inverted: hidden(Inverted) # type: ignore - GyreArchives: hidden(GyreArchives) # type: ignore - Cantoran: hidden(Cantoran) # type: ignore - LoreChecks: hidden(LoreChecks) # type: ignore - BossRando: hidden(BossRando) # type: ignore - DamageRando: hidden(DamageRando) # type: ignore +@dataclass +class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): + StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore + EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore + StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore + QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore + SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore + Inverted: OptionsHider.hidden(Inverted) # type: ignore + GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore + Cantoran: OptionsHider.hidden(Cantoran) # type: ignore + LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore + BossRando: OptionsHider.hidden(BossRando) # type: ignore + DamageRando: OptionsHider.hidden(DamageRando) # type: ignore DamageRandoOverrides: HiddenDamageRandoOverrides - HpCap: hidden(HpCap) # type: ignore - LevelCap: hidden(LevelCap) # type: ignore - ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore - BossHealing: hidden(BossHealing) # type: ignore - ShopFill: hidden(ShopFill) # type: ignore - ShopWarpShards: hidden(ShopWarpShards) # type: ignore - ShopMultiplier: hidden(ShopMultiplier) # type: ignore - LootPool: hidden(LootPool) # type: ignore - DropRateCategory: hidden(DropRateCategory) # type: ignore - FixedDropRate: hidden(FixedDropRate) # type: ignore - LootTierDistro: hidden(LootTierDistro) # type: ignore - ShowBestiary: hidden(ShowBestiary) # type: ignore - ShowDrops: hidden(ShowDrops) # type: ignore - EnterSandman: hidden(EnterSandman) # type: ignore - DadPercent: hidden(DadPercent) # type: ignore - RisingTides: hidden(RisingTides) # type: ignore + HpCap: OptionsHider.hidden(HpCap) # type: ignore + LevelCap: OptionsHider.hidden(LevelCap) # type: ignore + ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore + BossHealing: OptionsHider.hidden(BossHealing) # type: ignore + ShopFill: OptionsHider.hidden(ShopFill) # type: ignore + ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore + ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore + LootPool: OptionsHider.hidden(LootPool) # type: ignore + DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore + FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore + LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore + ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore + ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore + EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore + DadPercent: OptionsHider.hidden(DadPercent) # type: ignore + RisingTides: OptionsHider.hidden(RisingTides) # type: ignore RisingTidesOverrides: HiddenRisingTidesOverrides - UnchainedKeys: hidden(UnchainedKeys) # type: ignore - PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore - TrapChance: hidden(TrapChance) # type: ignore + UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: OptionsHider.hidden(TrapChance) # type: ignore Traps: HiddenTraps # type: ignore - DeathLink: hidden(DeathLink) # type: ignore + DeathLink: OptionsHider.hidden(DeathLink) # type: ignore def handle_backward_compatibility(self) -> None: has_replaced_options: bool = False From 1366e0f7f0250719e5bb2ace1ae7e41a9f978af5 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 2 Jun 2024 19:35:24 +0200 Subject: [PATCH 13/15] Replaced one more multiworld option usage --- worlds/timespinner/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index b3fa90056d4..28b6be06d3b 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -289,7 +289,7 @@ def place_first_progression_item(self, excluded_items: Set[str]) -> None: 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 From 08bf657988d80104a6a5dae0b8284d0b82138a2b Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 10 Jun 2024 20:37:39 +0200 Subject: [PATCH 14/15] Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/timespinner/Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 5bbf20fc0b2..82208bf0a71 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -609,4 +609,5 @@ def handle_backward_compatibility(self) -> None: has_replaced_options = True if has_replaced_options: - logging.warning("Timespinner options where renamed from PasCalCase to snake_case, plz update your yml") \ No newline at end of file + logging.warning("Timespinner options where renamed from PasCalCase to snake_case, plz update your yaml") + \ No newline at end of file From 2a2b3f1e03b8cbc5dd18ed30993542b13b9fc69b Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Mon, 10 Jun 2024 22:33:01 +0200 Subject: [PATCH 15/15] Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --- worlds/timespinner/LogicExtensions.py | 6 +- worlds/timespinner/Options.py | 82 +++++++++++++-------------- worlds/timespinner/__init__.py | 20 +++++-- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index 3335245b901..6c9cb3f684a 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -19,9 +19,9 @@ def __init__(self, player: int, options: Optional[TimespinnerOptions], precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = options and options.specific_keycards.value - self.flag_eye_spy = options and options.eye_spy.value - self.flag_unchained_keys = options and options.unchained_keys.value + self.flag_specific_keycards = bool(options and options.specific_keycards) + self.flag_eye_spy = bool(options and options.eye_spy) + self.flag_unchained_keys = bool(options and options.unchained_keys) if precalculated_weights: if self.flag_unchained_keys: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 82208bf0a71..20ad8132c45 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -4,7 +4,6 @@ from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions from schema import Schema, And, Optional, Or -import logging class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" @@ -425,6 +424,10 @@ def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: new_option.visibility = Visibility.none new_option.__doc__ = option.__doc__ return new_option + +class HasReplacedCamelCase(Toggle): + """For internal use will display a warning message if true""" + visibility = Visibility.none @dataclass class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): @@ -463,151 +466,146 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): TrapChance: OptionsHider.hidden(TrapChance) # type: ignore Traps: HiddenTraps # type: ignore DeathLink: OptionsHider.hidden(DeathLink) # type: ignore + has_replaced_options: HasReplacedCamelCase def handle_backward_compatibility(self) -> None: - has_replaced_options: bool = False - if self.StartWithJewelryBox != StartWithJewelryBox.default and \ self.start_with_jewelry_box == StartWithJewelryBox.default: self.start_with_jewelry_box.value = self.StartWithJewelryBox.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DownloadableItems != DownloadableItems.default and \ self.downloadable_items == DownloadableItems.default: self.downloadable_items.value = self.DownloadableItems.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.EyeSpy != EyeSpy.default and \ self.eye_spy == EyeSpy.default: self.eye_spy.value = self.EyeSpy.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.StartWithMeyef != StartWithMeyef.default and \ self.start_with_meyef == StartWithMeyef.default: self.start_with_meyef.value = self.StartWithMeyef.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.QuickSeed != QuickSeed.default and \ self.quick_seed == QuickSeed.default: self.quick_seed.value = self.QuickSeed.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.SpecificKeycards != SpecificKeycards.default and \ self.specific_keycards == SpecificKeycards.default: self.specific_keycards.value = self.SpecificKeycards.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.Inverted != Inverted.default and \ self.inverted == Inverted.default: self.inverted.value = self.Inverted.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.GyreArchives != GyreArchives.default and \ self.gyre_archives == GyreArchives.default: self.gyre_archives.value = self.GyreArchives.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.Cantoran != Cantoran.default and \ self.cantoran == Cantoran.default: self.cantoran.value = self.Cantoran.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.LoreChecks != LoreChecks.default and \ self.lore_checks == LoreChecks.default: self.lore_checks.value = self.LoreChecks.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.BossRando != BossRando.default and \ self.boss_rando == BossRando.default: self.boss_rando.value = self.BossRando.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DamageRando != DamageRando.default and \ self.damage_rando == DamageRando.default: self.damage_rando.value = self.DamageRando.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DamageRandoOverrides != DamageRandoOverrides.default and \ self.damage_rando_overrides == DamageRandoOverrides.default: self.damage_rando_overrides.value = self.DamageRandoOverrides.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.HpCap != HpCap.default and \ self.hp_cap == HpCap.default: self.hp_cap.value = self.HpCap.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.LevelCap != LevelCap.default and \ self.level_cap == LevelCap.default: self.level_cap.value = self.LevelCap.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ExtraEarringsXP != ExtraEarringsXP.default and \ self.extra_earrings_xp == ExtraEarringsXP.default: self.extra_earrings_xp.value = self.ExtraEarringsXP.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.BossHealing != BossHealing.default and \ self.boss_healing == BossHealing.default: self.boss_healing.value = self.BossHealing.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ShopFill != ShopFill.default and \ self.shop_fill == ShopFill.default: self.shop_fill.value = self.ShopFill.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ShopWarpShards != ShopWarpShards.default and \ self.shop_warp_shards == ShopWarpShards.default: self.shop_warp_shards.value = self.ShopWarpShards.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ShopMultiplier != ShopMultiplier.default and \ self.shop_multiplier == ShopMultiplier.default: self.shop_multiplier.value = self.ShopMultiplier.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.LootPool != LootPool.default and \ self.loot_pool == LootPool.default: self.loot_pool.value = self.LootPool.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DropRateCategory != DropRateCategory.default and \ self.drop_rate_category == DropRateCategory.default: self.drop_rate_category.value = self.DropRateCategory.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.FixedDropRate != FixedDropRate.default and \ self.fixed_drop_rate == FixedDropRate.default: self.fixed_drop_rate.value = self.FixedDropRate.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.LootTierDistro != LootTierDistro.default and \ self.loot_tier_distro == LootTierDistro.default: self.loot_tier_distro.value = self.LootTierDistro.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ShowBestiary != ShowBestiary.default and \ self.show_bestiary == ShowBestiary.default: self.show_bestiary.value = self.ShowBestiary.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.ShowDrops != ShowDrops.default and \ self.show_drops == ShowDrops.default: self.show_drops.value = self.ShowDrops.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.EnterSandman != EnterSandman.default and \ self.enter_sandman == EnterSandman.default: self.enter_sandman.value = self.EnterSandman.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DadPercent != DadPercent.default and \ self.dad_percent == DadPercent.default: self.dad_percent.value = self.DadPercent.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.RisingTides != RisingTides.default and \ self.rising_tides == RisingTides.default: self.rising_tides.value = self.RisingTides.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.RisingTidesOverrides != RisingTidesOverrides.default and \ self.rising_tides_overrides == RisingTidesOverrides.default: self.rising_tides_overrides.value = self.RisingTidesOverrides.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.UnchainedKeys != UnchainedKeys.default and \ self.unchained_keys == UnchainedKeys.default: self.unchained_keys.value = self.UnchainedKeys.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \ self.back_to_the_future == PresentAccessWithWheelAndSpindle.default: self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.TrapChance != TrapChance.default and \ self.trap_chance == TrapChance.default: self.trap_chance.value = self.TrapChance.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.Traps != Traps.default and \ self.traps == Traps.default: self.traps.value = self.Traps.value - has_replaced_options = True + self.has_replaced_options.value = Toggle.option_true if self.DeathLink != DeathLink.default and \ self.death_link == DeathLink.default: self.death_link.value = self.DeathLink.value - has_replaced_options = True - - if has_replaced_options: - logging.warning("Timespinner options where renamed from PasCalCase to snake_case, plz update your yaml") - \ No newline at end of file + self.has_replaced_options.value = Toggle.option_true diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 28b6be06d3b..66744cffdf8 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -7,6 +7,7 @@ from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld +import logging class TimespinnerWebWorld(WebWorld): theme = "ice" @@ -49,6 +50,8 @@ class TimespinnerWorld(World): precalculated_weights: PreCalculatedWeights def generate_early(self) -> None: + 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 @@ -59,8 +62,6 @@ def generate_early(self) -> None: if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: self.options.start_with_jewelry_box.value = Toggle.option_true - self.options.handle_backward_compatibility() - def create_regions(self) -> None: create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) @@ -187,6 +188,15 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: 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] @@ -245,9 +255,9 @@ def get_excluded_items(self) -> Set[str]: excluded_items.add('Modern Warp Beacon') excluded_items.add('Mysterious Warp Beacon') - for item_name in self.options.start_inventory.value.keys(): - if item_name not in self.item_name_groups['UseItem']: - excluded_items.add(item_name) + for item in self.multiworld.precollected_items[self.player]: + if item.name not in self.item_name_groups['UseItem']: + excluded_items.add(item.name) return excluded_items