Timespinner: migrate to new options api and correct random (#2485)
* Implemented new options system into Timespinner * Fixed typo * Fixed typo * Fixed slotdata maybe * Fixes * more fixes * Fixed failing unit tests * Implemented options backwards comnpatibility * Fixed option fallbacks * Implemented review results * Fixed logic bug * Fixed python 3.8/3.9 compatibility * Replaced one more multiworld option usage * Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									77e3f9fbef
								
							
						
					
					
						commit
						1d19da0c76
					
				|  | @ -1,6 +1,6 @@ | |||
| from typing import List, 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,10 @@ class LocationData(NamedTuple): | |||
|     rule: Optional[Callable[[CollectionState], bool]] = None | ||||
| 
 | ||||
| 
 | ||||
| def get_location_datas(world: Optional[MultiWorld], player: Optional[int],  | ||||
|                   precalculated_weights: PreCalculatedWeights) -> List[LocationData]: | ||||
| 
 | ||||
|     flooded: PreCalculatedWeights = precalculated_weights | ||||
|     logic = TimespinnerLogic(world, player, precalculated_weights) | ||||
| def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions], | ||||
|                   precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]: | ||||
|     flooded: Optional[PreCalculatedWeights] = precalculated_weights | ||||
|     logic = TimespinnerLogic(player, options, precalculated_weights) | ||||
| 
 | ||||
|     # 1337000 - 1337155 Generic locations | ||||
|     # 1337171 - 1337175 New Pickup checks | ||||
|  | @ -203,7 +202,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 +222,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 +257,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), | ||||
|  |  | |||
|  | @ -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 = 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: | ||||
|  |  | |||
|  | @ -1,59 +1,50 @@ | |||
| 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 Type, Any | ||||
| from typing import Dict | ||||
| 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 | ||||
| 
 | ||||
| 
 | ||||
| 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" | ||||
|  | @ -62,7 +53,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" | ||||
|  | @ -72,7 +62,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" | ||||
|  | @ -85,7 +74,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""" | ||||
|  | @ -191,7 +179,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" | ||||
|  | @ -199,7 +186,6 @@ class HpCap(Range): | |||
|     range_end = 999 | ||||
|     default = 999 | ||||
| 
 | ||||
| 
 | ||||
| class LevelCap(Range): | ||||
|     """Sets the max level Lunais can achieve.""" | ||||
|     display_name = "Level Cap" | ||||
|  | @ -207,7 +193,6 @@ class LevelCap(Range): | |||
|     range_end = 99 | ||||
|     default = 99 | ||||
| 
 | ||||
| 
 | ||||
| class ExtraEarringsXP(Range): | ||||
|     """Adds additional XP granted by Galaxy Earrings.""" | ||||
|     display_name = "Extra Earrings XP" | ||||
|  | @ -215,12 +200,10 @@ class ExtraEarringsXP(Range): | |||
|     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. | ||||
|  | @ -233,12 +216,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" | ||||
|  | @ -246,7 +227,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 | ||||
|  | @ -257,7 +237,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 | ||||
|  | @ -271,7 +250,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" | ||||
|  | @ -279,7 +257,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 | ||||
|  | @ -291,32 +268,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 { | ||||
|  | @ -341,7 +312,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""" | ||||
|  | @ -373,13 +343,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""" | ||||
|  | @ -388,67 +356,256 @@ 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): | ||||
|     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 | ||||
|     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 | ||||
|     back_to_the_future: PresentAccessWithWheelAndSpindle | ||||
|     trap_chance: TrapChance | ||||
|     traps: Traps | ||||
| 
 | ||||
| # 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, | ||||
|     "EnemyRando": EnemyRando, | ||||
|     "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, | ||||
|     "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, | ||||
|     "DeathLink": DeathLink, | ||||
| } | ||||
| 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 | ||||
| 
 | ||||
| def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: | ||||
|     return get_option_value(world, player, name) > 0 | ||||
| class HiddenTraps(Traps): | ||||
|     """List of traps that may be in the item pool to find""" | ||||
|     visibility = Visibility.none | ||||
| 
 | ||||
| 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 | ||||
|      | ||||
| def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: | ||||
|     option = getattr(world, name, None) | ||||
|     if option == None: | ||||
|         return 0 | ||||
| class HasReplacedCamelCase(Toggle): | ||||
|     """For internal use will display a warning message if true""" | ||||
|     visibility = Visibility.none | ||||
| 
 | ||||
|     return option[player].value | ||||
| @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: 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: OptionsHider.hidden(UnchainedKeys) # type: ignore | ||||
|     PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore | ||||
|     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: | ||||
|         if self.StartWithJewelryBox != StartWithJewelryBox.default and \ | ||||
|             self.start_with_jewelry_box == StartWithJewelryBox.default: | ||||
|             self.start_with_jewelry_box.value = self.StartWithJewelryBox.value | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             self.has_replaced_options.value = Toggle.option_true | ||||
|         if self.Inverted != Inverted.default and \ | ||||
|             self.inverted == Inverted.default: | ||||
|             self.inverted.value = self.Inverted.value | ||||
|             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 | ||||
|             self.has_replaced_options.value = Toggle.option_true | ||||
|         if self.Cantoran != Cantoran.default and \ | ||||
|             self.cantoran == Cantoran.default: | ||||
|             self.cantoran.value = self.Cantoran.value | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             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 | ||||
|             self.has_replaced_options.value = Toggle.option_true | ||||
|         if self.Traps != Traps.default and \ | ||||
|             self.traps == Traps.default: | ||||
|             self.traps.value = self.Traps.value | ||||
|             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 | ||||
|             self.has_replaced_options.value = Toggle.option_true | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| from typing import Tuple, Dict, Union, List | ||||
| 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 | ||||
|  | @ -21,22 +21,22 @@ class PreCalculatedWeights: | |||
|     flood_lake_serene_bridge: bool | ||||
|     flood_lab: 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") | ||||
|             self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") | ||||
|             self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") | ||||
|             self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") | ||||
|                 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") | ||||
|             self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene") | ||||
|             self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge") | ||||
|             self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab") | ||||
|         else: | ||||
|             self.flood_basement = False | ||||
|             self.flood_basement_high = False | ||||
|  | @ -52,10 +52,12 @@ class PreCalculatedWeights: | |||
|             self.flood_lab = 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.flood_xarion) | ||||
|             self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: | ||||
|     def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random, | ||||
|                                  is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: | ||||
|          | ||||
|         present_teleportation_gates: List[str] = [ | ||||
|             "GateKittyBoss", | ||||
|             "GateLeftLibrary", | ||||
|  | @ -80,38 +82,30 @@ class PreCalculatedWeights: | |||
|             "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.append("GateMaw") | ||||
| 
 | ||||
|         if not is_xarion_flooded: | ||||
|             present_teleportation_gates.append("GateXarion") | ||||
| 
 | ||||
|         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 | ||||
|  | @ -123,13 +117,13 @@ class PreCalculatedWeights: | |||
|         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 | ||||
|          | ||||
|  |  | |||
|  | @ -1,14 +1,16 @@ | |||
| from typing import List, Set, Dict, 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): | ||||
| def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions, | ||||
|                                  precalculated_weights: PreCalculatedWeights): | ||||
| 
 | ||||
|     locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( | ||||
|         get_location_datas(world, player, precalculated_weights)) | ||||
|         get_location_datas(player, options, precalculated_weights)) | ||||
| 
 | ||||
|     regions = [ | ||||
|         create_region(world, player, locations_per_region, 'Menu'), | ||||
|  | @ -53,7 +55,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'), | ||||
|  | @ -64,10 +66,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: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) | ||||
|     connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") | ||||
|  | @ -123,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w | |||
|     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.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)) | ||||
|  | @ -178,11 +180,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), "Refugee Camp") | ||||
|  | @ -220,12 +222,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.inverted: | ||||
|         starting_region = world.get_region('Refugee Camp', player) | ||||
|     else: | ||||
|         starting_region = world.get_region('Lake desolation', player) | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| from typing import Dict, List, Set, Tuple, TextIO, Union | ||||
| from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification | ||||
| from typing import Dict, List, Set, Tuple, TextIO | ||||
| from BaseClasses import Item, Tutorial, ItemClassification | ||||
| from .Items import get_item_names_per_category | ||||
| from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items | ||||
| from .Locations import get_location_datas, EventId | ||||
| from .Options import is_option_enabled, get_option_value, timespinner_options | ||||
| from .Options import BackwardsCompatiableTimespinnerOptions, Toggle | ||||
| from .PreCalculatedWeights import PreCalculatedWeights | ||||
| from .Regions import create_regions_and_locations | ||||
| from worlds.AutoWorld import World, WebWorld | ||||
| import logging | ||||
| 
 | ||||
| class TimespinnerWebWorld(WebWorld): | ||||
|     theme = "ice" | ||||
|  | @ -35,32 +36,34 @@ class TimespinnerWorld(World): | |||
|     Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. | ||||
|     Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. | ||||
|     """ | ||||
| 
 | ||||
|     option_definitions = timespinner_options | ||||
|     options_dataclass = BackwardsCompatiableTimespinnerOptions | ||||
|     options: BackwardsCompatiableTimespinnerOptions | ||||
|     game = "Timespinner" | ||||
|     topology_present = True | ||||
|     web = TimespinnerWebWorld() | ||||
|     required_client_version = (0, 4, 2) | ||||
| 
 | ||||
|     item_name_to_id = {name: data.code for name, data in item_table.items()} | ||||
|     location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} | ||||
|     location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)} | ||||
|     item_name_groups = get_item_names_per_category() | ||||
| 
 | ||||
|     precalculated_weights: PreCalculatedWeights | ||||
| 
 | ||||
|     def generate_early(self) -> None: | ||||
|         self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) | ||||
|         self.options.handle_backward_compatibility() | ||||
| 
 | ||||
|         self.precalculated_weights = PreCalculatedWeights(self.options, self.random) | ||||
| 
 | ||||
|         # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly | ||||
|         if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: | ||||
|             self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true | ||||
|         if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0: | ||||
|             self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true | ||||
|         if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: | ||||
|             self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true | ||||
|         if self.options.start_inventory.value.pop('Meyef', 0) > 0: | ||||
|             self.options.start_with_meyef.value = Toggle.option_true | ||||
|         if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0: | ||||
|             self.options.quick_seed.value = Toggle.option_true | ||||
|         if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: | ||||
|             self.options.start_with_jewelry_box.value = Toggle.option_true | ||||
| 
 | ||||
|     def create_regions(self) -> None:  | ||||
|         create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) | ||||
|         create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) | ||||
| 
 | ||||
|     def create_items(self) -> None:  | ||||
|         self.create_and_assign_event_items() | ||||
|  | @ -74,7 +77,7 @@ class TimespinnerWorld(World): | |||
| 
 | ||||
|     def set_rules(self) -> None: | ||||
|         final_boss: str | ||||
|         if self.is_option_enabled("DadPercent"): | ||||
|         if self.options.dad_percent: | ||||
|             final_boss = "Killed Emperor" | ||||
|         else: | ||||
|             final_boss = "Killed Nightmare" | ||||
|  | @ -82,48 +85,74 @@ class TimespinnerWorld(World): | |||
|         self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player)  | ||||
| 
 | ||||
|     def fill_slot_data(self) -> Dict[str, object]: | ||||
|         slot_data: Dict[str, object] = {} | ||||
| 
 | ||||
|         ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"} | ||||
| 
 | ||||
|         for option_name in timespinner_options: | ||||
|             if (option_name not in ap_specific_settings): | ||||
|                 slot_data[option_name] = self.get_option_value(option_name) | ||||
| 
 | ||||
|         slot_data["StinkyMaw"] = True | ||||
|         slot_data["ProgressiveVerticalMovement"] = False | ||||
|         slot_data["ProgressiveKeycards"] = False | ||||
|         slot_data["PersonalItems"] = self.get_personal_items() | ||||
|         slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock | ||||
|         slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock | ||||
|         slot_data["PastGate"] = self.precalculated_weights.past_key_unlock | ||||
|         slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock | ||||
|         slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \ | ||||
|                                 int(self.precalculated_weights.flood_basement_high) | ||||
|         slot_data["Xarion"] = self.precalculated_weights.flood_xarion | ||||
|         slot_data["Maw"] = self.precalculated_weights.flood_maw | ||||
|         slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft | ||||
|         slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back | ||||
|         slot_data["CastleMoat"] = self.precalculated_weights.flood_moat | ||||
|         slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard | ||||
|         slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation | ||||
|         slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene | ||||
|         slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge | ||||
|         slot_data["Lab"] = self.precalculated_weights.flood_lab | ||||
| 
 | ||||
|         return slot_data | ||||
|         return { | ||||
|             # options | ||||
|             "StartWithJewelryBox": self.options.start_with_jewelry_box.value, | ||||
|             "DownloadableItems": self.options.downloadable_items.value, | ||||
|             "EyeSpy": self.options.eye_spy.value, | ||||
|             "StartWithMeyef": self.options.start_with_meyef.value, | ||||
|             "QuickSeed": self.options.quick_seed.value, | ||||
|             "SpecificKeycards": self.options.specific_keycards.value, | ||||
|             "Inverted": self.options.inverted.value, | ||||
|             "GyreArchives": self.options.gyre_archives.value, | ||||
|             "Cantoran": self.options.cantoran.value, | ||||
|             "LoreChecks": self.options.lore_checks.value, | ||||
|             "BossRando": self.options.boss_rando.value, | ||||
|             "DamageRando": self.options.damage_rando.value, | ||||
|             "DamageRandoOverrides": self.options.damage_rando_overrides.value, | ||||
|             "HpCap": self.options.hp_cap.value, | ||||
|             "LevelCap": self.options.level_cap.value, | ||||
|             "ExtraEarringsXP": self.options.extra_earrings_xp.value, | ||||
|             "BossHealing": self.options.boss_healing.value, | ||||
|             "ShopFill": self.options.shop_fill.value, | ||||
|             "ShopWarpShards": self.options.shop_warp_shards.value, | ||||
|             "ShopMultiplier": self.options.shop_multiplier.value, | ||||
|             "LootPool": self.options.loot_pool.value, | ||||
|             "DropRateCategory": self.options.drop_rate_category.value, | ||||
|             "FixedDropRate": self.options.fixed_drop_rate.value, | ||||
|             "LootTierDistro": self.options.loot_tier_distro.value, | ||||
|             "ShowBestiary": self.options.show_bestiary.value, | ||||
|             "ShowDrops": self.options.show_drops.value, | ||||
|             "EnterSandman": self.options.enter_sandman.value, | ||||
|             "DadPercent": self.options.dad_percent.value, | ||||
|             "RisingTides": self.options.rising_tides.value, | ||||
|             "UnchainedKeys": self.options.unchained_keys.value, | ||||
|             "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, | ||||
|             "Traps": self.options.traps.value, | ||||
|             "DeathLink": self.options.death_link.value, | ||||
|             "StinkyMaw": True, | ||||
|             # data | ||||
|             "PersonalItems": self.get_personal_items(), | ||||
|             "PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock, | ||||
|             "PresentGate": self.precalculated_weights.present_key_unlock, | ||||
|             "PastGate": self.precalculated_weights.past_key_unlock, | ||||
|             "TimeGate": self.precalculated_weights.time_key_unlock, | ||||
|             # rising tides | ||||
|             "Basement": int(self.precalculated_weights.flood_basement) + \ | ||||
|                                     int(self.precalculated_weights.flood_basement_high), | ||||
|             "Xarion": self.precalculated_weights.flood_xarion, | ||||
|             "Maw": self.precalculated_weights.flood_maw, | ||||
|             "PyramidShaft": self.precalculated_weights.flood_pyramid_shaft, | ||||
|             "BackPyramid": self.precalculated_weights.flood_pyramid_back, | ||||
|             "CastleMoat": self.precalculated_weights.flood_moat, | ||||
|             "CastleCourtyard": self.precalculated_weights.flood_courtyard, | ||||
|             "LakeDesolation": self.precalculated_weights.flood_lake_desolation, | ||||
|             "DryLakeSerene": not self.precalculated_weights.flood_lake_serene, | ||||
|             "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge, | ||||
|             "Lab": self.precalculated_weights.flood_lab | ||||
|         } | ||||
| 
 | ||||
|     def write_spoiler_header(self, spoiler_handle: TextIO) -> None: | ||||
|         if self.is_option_enabled("UnchainedKeys"): | ||||
|         if self.options.unchained_keys: | ||||
|             spoiler_handle.write(f'Modern Warp Beacon unlock:       {self.precalculated_weights.present_key_unlock}\n') | ||||
|             spoiler_handle.write(f'Timeworn Warp Beacon unlock:     {self.precalculated_weights.past_key_unlock}\n') | ||||
| 
 | ||||
|             if self.is_option_enabled("EnterSandman"): | ||||
|             if self.options.enter_sandman: | ||||
|                 spoiler_handle.write(f'Mysterious Warp Beacon unlock:   {self.precalculated_weights.time_key_unlock}\n') | ||||
|         else: | ||||
|             spoiler_handle.write(f'Twin Pyramid Keys unlock:        {self.precalculated_weights.pyramid_keys_unlock}\n') | ||||
|         | ||||
|         if self.is_option_enabled("RisingTides"): | ||||
|         if self.options.rising_tides: | ||||
|             flooded_areas: List[str] = [] | ||||
| 
 | ||||
|             if self.precalculated_weights.flood_basement: | ||||
|  | @ -159,6 +188,15 @@ class TimespinnerWorld(World): | |||
| 
 | ||||
|             spoiler_handle.write(f'Flooded Areas:                   {flooded_areas_string}\n') | ||||
| 
 | ||||
|         if self.options.has_replaced_options: | ||||
|             warning = \ | ||||
|                 f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ | ||||
|                 "please update your yaml" | ||||
| 
 | ||||
|             spoiler_handle.write("\n") | ||||
|             spoiler_handle.write(warning) | ||||
|             logging.warning(warning) | ||||
| 
 | ||||
|     def create_item(self, name: str) -> Item: | ||||
|         data = item_table[name] | ||||
| 
 | ||||
|  | @ -176,41 +214,41 @@ class TimespinnerWorld(World): | |||
|         if not item.advancement: | ||||
|             return item | ||||
| 
 | ||||
|         if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): | ||||
|         if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items: | ||||
|             item.classification = ItemClassification.filler | ||||
|         elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): | ||||
|         elif name == 'Oculus Ring' and not self.options.eye_spy: | ||||
|             item.classification = ItemClassification.filler | ||||
|         elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): | ||||
|         elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives: | ||||
|             item.classification = ItemClassification.filler | ||||
|         elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ | ||||
|                 and not self.is_option_enabled("UnchainedKeys"): | ||||
|                 and not self.options.unchained_keys: | ||||
|             item.classification = ItemClassification.filler | ||||
| 
 | ||||
|         return item | ||||
| 
 | ||||
|     def get_filler_item_name(self) -> str: | ||||
|         trap_chance: int = self.get_option_value("TrapChance") | ||||
|         enabled_traps: List[str] = self.get_option_value("Traps") | ||||
|         trap_chance: int = self.options.trap_chance.value | ||||
|         enabled_traps: List[str] = self.options.traps.value | ||||
| 
 | ||||
|         if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: | ||||
|             return self.multiworld.random.choice(enabled_traps) | ||||
|         if self.random.random() < (trap_chance / 100) and enabled_traps: | ||||
|             return self.random.choice(enabled_traps) | ||||
|         else: | ||||
|             return self.multiworld.random.choice(filler_items)  | ||||
|             return self.random.choice(filler_items)  | ||||
| 
 | ||||
|     def get_excluded_items(self) -> Set[str]: | ||||
|         excluded_items: Set[str] = set() | ||||
| 
 | ||||
|         if self.is_option_enabled("StartWithJewelryBox"): | ||||
|         if self.options.start_with_jewelry_box: | ||||
|             excluded_items.add('Jewelry Box') | ||||
|         if self.is_option_enabled("StartWithMeyef"): | ||||
|         if self.options.start_with_meyef: | ||||
|             excluded_items.add('Meyef') | ||||
|         if self.is_option_enabled("QuickSeed"): | ||||
|         if self.options.quick_seed: | ||||
|             excluded_items.add('Talaria Attachment') | ||||
| 
 | ||||
|         if self.is_option_enabled("UnchainedKeys"): | ||||
|         if self.options.unchained_keys: | ||||
|             excluded_items.add('Twin Pyramid Key') | ||||
| 
 | ||||
|             if not self.is_option_enabled("EnterSandman"): | ||||
|             if not self.options.enter_sandman: | ||||
|                 excluded_items.add('Mysterious Warp Beacon') | ||||
|         else: | ||||
|             excluded_items.add('Timeworn Warp Beacon') | ||||
|  | @ -224,8 +262,8 @@ class TimespinnerWorld(World): | |||
|         return excluded_items | ||||
| 
 | ||||
|     def assign_starter_items(self, excluded_items: Set[str]) -> None: | ||||
|         non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value | ||||
|         local_items: Set[str] = self.multiworld.local_items[self.player].value | ||||
|         non_local_items: Set[str] = self.options.non_local_items.value | ||||
|         local_items: Set[str] = self.options.local_items.value | ||||
| 
 | ||||
|         local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if | ||||
|                                             item in local_items or not item in non_local_items) | ||||
|  | @ -247,27 +285,26 @@ class TimespinnerWorld(World): | |||
|         self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) | ||||
| 
 | ||||
|     def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: | ||||
|         item_name = self.multiworld.random.choice(item_list) | ||||
|         item_name = self.random.choice(item_list) | ||||
| 
 | ||||
|         self.place_locked_item(excluded_items, location, item_name) | ||||
| 
 | ||||
|     def place_first_progression_item(self, excluded_items: Set[str]) -> None: | ||||
|         if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ | ||||
|                 or self.precalculated_weights.flood_lake_desolation: | ||||
|         if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation: | ||||
|             return | ||||
| 
 | ||||
|         for item in self.multiworld.precollected_items[self.player]: | ||||
|             if item.name in starter_progression_items and not item.name in excluded_items: | ||||
|         for item_name in self.options.start_inventory.value.keys(): | ||||
|             if item_name in starter_progression_items: | ||||
|                 return | ||||
| 
 | ||||
|         local_starter_progression_items = tuple( | ||||
|             item for item in starter_progression_items  | ||||
|                 if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) | ||||
|                 if item not in excluded_items and item not in self.options.non_local_items.value) | ||||
| 
 | ||||
|         if not local_starter_progression_items: | ||||
|             return | ||||
| 
 | ||||
|         progression_item = self.multiworld.random.choice(local_starter_progression_items) | ||||
|         progression_item = self.random.choice(local_starter_progression_items) | ||||
| 
 | ||||
|         self.multiworld.local_early_items[self.player][progression_item] = 1 | ||||
| 
 | ||||
|  | @ -307,9 +344,3 @@ class TimespinnerWorld(World): | |||
|                 personal_items[location.address] = location.item.code | ||||
| 
 | ||||
|         return personal_items | ||||
|      | ||||
|     def is_option_enabled(self, option: str) -> bool: | ||||
|         return is_option_enabled(self.multiworld, self.player, option) | ||||
| 
 | ||||
|     def get_option_value(self, option: str) -> Union[int, Dict, List]: | ||||
|         return get_option_value(self.multiworld, self.player, option) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue