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 typing import List, Optional, Callable, NamedTuple
|
||||||
from BaseClasses import MultiWorld, CollectionState
|
from BaseClasses import CollectionState
|
||||||
from .Options import is_option_enabled
|
from .Options import TimespinnerOptions
|
||||||
from .PreCalculatedWeights import PreCalculatedWeights
|
from .PreCalculatedWeights import PreCalculatedWeights
|
||||||
from .LogicExtensions import TimespinnerLogic
|
from .LogicExtensions import TimespinnerLogic
|
||||||
|
|
||||||
|
@ -14,11 +14,10 @@ class LocationData(NamedTuple):
|
||||||
rule: Optional[Callable[[CollectionState], bool]] = None
|
rule: Optional[Callable[[CollectionState], bool]] = None
|
||||||
|
|
||||||
|
|
||||||
def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
|
def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions],
|
||||||
precalculated_weights: PreCalculatedWeights) -> List[LocationData]:
|
precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]:
|
||||||
|
flooded: Optional[PreCalculatedWeights] = precalculated_weights
|
||||||
flooded: PreCalculatedWeights = precalculated_weights
|
logic = TimespinnerLogic(player, options, precalculated_weights)
|
||||||
logic = TimespinnerLogic(world, player, precalculated_weights)
|
|
||||||
|
|
||||||
# 1337000 - 1337155 Generic locations
|
# 1337000 - 1337155 Generic locations
|
||||||
# 1337171 - 1337175 New Pickup checks
|
# 1337171 - 1337175 New Pickup checks
|
||||||
|
@ -203,7 +202,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int],
|
||||||
]
|
]
|
||||||
|
|
||||||
# 1337156 - 1337170 Downloads
|
# 1337156 - 1337170 Downloads
|
||||||
if not world or is_option_enabled(world, player, "DownloadableItems"):
|
if not options or options.downloadable_items:
|
||||||
location_table += (
|
location_table += (
|
||||||
LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)),
|
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)),
|
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
|
# 1337176 - 1337176 Cantoran
|
||||||
if not world or is_option_enabled(world, player, "Cantoran"):
|
if not options or options.cantoran:
|
||||||
location_table += (
|
location_table += (
|
||||||
LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176),
|
LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1337177 - 1337198 Lore Checks
|
# 1337177 - 1337198 Lore Checks
|
||||||
if not world or is_option_enabled(world, player, "LoreChecks"):
|
if not options or options.lore_checks:
|
||||||
location_table += (
|
location_table += (
|
||||||
LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177),
|
LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177),
|
||||||
LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178),
|
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
|
# 1337199 - 1337236 Reserved for future use
|
||||||
|
|
||||||
# 1337237 - 1337245 GyreArchives
|
# 1337237 - 1337245 GyreArchives
|
||||||
if not world or is_option_enabled(world, player, "GyreArchives"):
|
if not options or options.gyre_archives:
|
||||||
location_table += (
|
location_table += (
|
||||||
LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237),
|
LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237),
|
||||||
LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238),
|
LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Union
|
from typing import Union, Optional
|
||||||
from BaseClasses import MultiWorld, CollectionState
|
from BaseClasses import CollectionState
|
||||||
from .Options import is_option_enabled
|
from .Options import TimespinnerOptions
|
||||||
from .PreCalculatedWeights import PreCalculatedWeights
|
from .PreCalculatedWeights import PreCalculatedWeights
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,17 +10,18 @@ class TimespinnerLogic:
|
||||||
flag_unchained_keys: bool
|
flag_unchained_keys: bool
|
||||||
flag_eye_spy: bool
|
flag_eye_spy: bool
|
||||||
flag_specific_keycards: bool
|
flag_specific_keycards: bool
|
||||||
pyramid_keys_unlock: Union[str, None]
|
pyramid_keys_unlock: Optional[str]
|
||||||
present_keys_unlock: Union[str, None]
|
present_keys_unlock: Optional[str]
|
||||||
past_keys_unlock: Union[str, None]
|
past_keys_unlock: Optional[str]
|
||||||
time_keys_unlock: Union[str, None]
|
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.player = player
|
||||||
|
|
||||||
self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards")
|
self.flag_specific_keycards = bool(options and options.specific_keycards)
|
||||||
self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy")
|
self.flag_eye_spy = bool(options and options.eye_spy)
|
||||||
self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys")
|
self.flag_unchained_keys = bool(options and options.unchained_keys)
|
||||||
|
|
||||||
if precalculated_weights:
|
if precalculated_weights:
|
||||||
if self.flag_unchained_keys:
|
if self.flag_unchained_keys:
|
||||||
|
|
|
@ -1,59 +1,50 @@
|
||||||
from typing import Dict, Union, List
|
from dataclasses import dataclass
|
||||||
from BaseClasses import MultiWorld
|
from typing import Type, Any
|
||||||
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList
|
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
|
from schema import Schema, And, Optional, Or
|
||||||
|
|
||||||
|
|
||||||
class StartWithJewelryBox(Toggle):
|
class StartWithJewelryBox(Toggle):
|
||||||
"Start with Jewelry Box unlocked"
|
"Start with Jewelry Box unlocked"
|
||||||
display_name = "Start with Jewelry Box"
|
display_name = "Start with Jewelry Box"
|
||||||
|
|
||||||
|
|
||||||
class DownloadableItems(DefaultOnToggle):
|
class DownloadableItems(DefaultOnToggle):
|
||||||
"With the tablet you will be able to download items at terminals"
|
"With the tablet you will be able to download items at terminals"
|
||||||
display_name = "Downloadable items"
|
display_name = "Downloadable items"
|
||||||
|
|
||||||
|
|
||||||
class EyeSpy(Toggle):
|
class EyeSpy(Toggle):
|
||||||
"Requires Oculus Ring in inventory to be able to break hidden walls."
|
"Requires Oculus Ring in inventory to be able to break hidden walls."
|
||||||
display_name = "Eye Spy"
|
display_name = "Eye Spy"
|
||||||
|
|
||||||
|
|
||||||
class StartWithMeyef(Toggle):
|
class StartWithMeyef(Toggle):
|
||||||
"Start with Meyef, ideal for when you want to play multiplayer."
|
"Start with Meyef, ideal for when you want to play multiplayer."
|
||||||
display_name = "Start with Meyef"
|
display_name = "Start with Meyef"
|
||||||
|
|
||||||
|
|
||||||
class QuickSeed(Toggle):
|
class QuickSeed(Toggle):
|
||||||
"Start with Talaria Attachment, Nyoom!"
|
"Start with Talaria Attachment, Nyoom!"
|
||||||
display_name = "Quick seed"
|
display_name = "Quick seed"
|
||||||
|
|
||||||
|
|
||||||
class SpecificKeycards(Toggle):
|
class SpecificKeycards(Toggle):
|
||||||
"Keycards can only open corresponding doors"
|
"Keycards can only open corresponding doors"
|
||||||
display_name = "Specific Keycards"
|
display_name = "Specific Keycards"
|
||||||
|
|
||||||
|
|
||||||
class Inverted(Toggle):
|
class Inverted(Toggle):
|
||||||
"Start in the past"
|
"Start in the past"
|
||||||
display_name = "Inverted"
|
display_name = "Inverted"
|
||||||
|
|
||||||
|
|
||||||
class GyreArchives(Toggle):
|
class GyreArchives(Toggle):
|
||||||
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
|
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
|
||||||
display_name = "Gyre Archives"
|
display_name = "Gyre Archives"
|
||||||
|
|
||||||
|
|
||||||
class Cantoran(Toggle):
|
class Cantoran(Toggle):
|
||||||
"Cantoran's fight and check are available upon revisiting his room"
|
"Cantoran's fight and check are available upon revisiting his room"
|
||||||
display_name = "Cantoran"
|
display_name = "Cantoran"
|
||||||
|
|
||||||
|
|
||||||
class LoreChecks(Toggle):
|
class LoreChecks(Toggle):
|
||||||
"Memories and journal entries contain items."
|
"Memories and journal entries contain items."
|
||||||
display_name = "Lore Checks"
|
display_name = "Lore Checks"
|
||||||
|
|
||||||
|
|
||||||
class BossRando(Choice):
|
class BossRando(Choice):
|
||||||
"Wheter all boss locations are shuffled, and if their damage/hp should be scaled."
|
"Wheter all boss locations are shuffled, and if their damage/hp should be scaled."
|
||||||
display_name = "Boss Randomization"
|
display_name = "Boss Randomization"
|
||||||
|
@ -62,7 +53,6 @@ class BossRando(Choice):
|
||||||
option_unscaled = 2
|
option_unscaled = 2
|
||||||
alias_true = 1
|
alias_true = 1
|
||||||
|
|
||||||
|
|
||||||
class EnemyRando(Choice):
|
class EnemyRando(Choice):
|
||||||
"Wheter enemies will be randomized, and if their damage/hp should be scaled."
|
"Wheter enemies will be randomized, and if their damage/hp should be scaled."
|
||||||
display_name = "Enemy Randomization"
|
display_name = "Enemy Randomization"
|
||||||
|
@ -72,7 +62,6 @@ class EnemyRando(Choice):
|
||||||
option_ryshia = 3
|
option_ryshia = 3
|
||||||
alias_true = 1
|
alias_true = 1
|
||||||
|
|
||||||
|
|
||||||
class DamageRando(Choice):
|
class DamageRando(Choice):
|
||||||
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
|
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
|
||||||
display_name = "Damage Rando"
|
display_name = "Damage Rando"
|
||||||
|
@ -85,7 +74,6 @@ class DamageRando(Choice):
|
||||||
option_manual = 6
|
option_manual = 6
|
||||||
alias_true = 2
|
alias_true = 2
|
||||||
|
|
||||||
|
|
||||||
class DamageRandoOverrides(OptionDict):
|
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
|
"""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"""
|
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 },
|
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HpCap(Range):
|
class HpCap(Range):
|
||||||
"Sets the number that Lunais's HP maxes out at."
|
"Sets the number that Lunais's HP maxes out at."
|
||||||
display_name = "HP Cap"
|
display_name = "HP Cap"
|
||||||
|
@ -199,7 +186,6 @@ class HpCap(Range):
|
||||||
range_end = 999
|
range_end = 999
|
||||||
default = 999
|
default = 999
|
||||||
|
|
||||||
|
|
||||||
class LevelCap(Range):
|
class LevelCap(Range):
|
||||||
"""Sets the max level Lunais can achieve."""
|
"""Sets the max level Lunais can achieve."""
|
||||||
display_name = "Level Cap"
|
display_name = "Level Cap"
|
||||||
|
@ -207,20 +193,17 @@ class LevelCap(Range):
|
||||||
range_end = 99
|
range_end = 99
|
||||||
default = 99
|
default = 99
|
||||||
|
|
||||||
|
|
||||||
class ExtraEarringsXP(Range):
|
class ExtraEarringsXP(Range):
|
||||||
"""Adds additional XP granted by Galaxy Earrings."""
|
"""Adds additional XP granted by Galaxy Earrings."""
|
||||||
display_name = "Extra Earrings XP"
|
display_name = "Extra Earrings XP"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 24
|
range_end = 24
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class BossHealing(DefaultOnToggle):
|
class BossHealing(DefaultOnToggle):
|
||||||
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
|
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
|
||||||
display_name = "Heal After Bosses"
|
display_name = "Heal After Bosses"
|
||||||
|
|
||||||
|
|
||||||
class ShopFill(Choice):
|
class ShopFill(Choice):
|
||||||
"""Sets the items for sale in Merchant Crow's shops.
|
"""Sets the items for sale in Merchant Crow's shops.
|
||||||
Default: No sunglasses or trendy jacket, but sand vials for sale.
|
Default: No sunglasses or trendy jacket, but sand vials for sale.
|
||||||
|
@ -233,12 +216,10 @@ class ShopFill(Choice):
|
||||||
option_vanilla = 2
|
option_vanilla = 2
|
||||||
option_empty = 3
|
option_empty = 3
|
||||||
|
|
||||||
|
|
||||||
class ShopWarpShards(DefaultOnToggle):
|
class ShopWarpShards(DefaultOnToggle):
|
||||||
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
|
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
|
||||||
display_name = "Always Sell Warp Shards"
|
display_name = "Always Sell Warp Shards"
|
||||||
|
|
||||||
|
|
||||||
class ShopMultiplier(Range):
|
class ShopMultiplier(Range):
|
||||||
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
|
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
|
||||||
display_name = "Shop Price Multiplier"
|
display_name = "Shop Price Multiplier"
|
||||||
|
@ -246,7 +227,6 @@ class ShopMultiplier(Range):
|
||||||
range_end = 10
|
range_end = 10
|
||||||
default = 1
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
class LootPool(Choice):
|
class LootPool(Choice):
|
||||||
"""Sets the items that drop from enemies (does not apply to boss reward checks)
|
"""Sets the items that drop from enemies (does not apply to boss reward checks)
|
||||||
Vanilla: Drops are the same as the base game
|
Vanilla: Drops are the same as the base game
|
||||||
|
@ -257,7 +237,6 @@ class LootPool(Choice):
|
||||||
option_randomized = 1
|
option_randomized = 1
|
||||||
option_empty = 2
|
option_empty = 2
|
||||||
|
|
||||||
|
|
||||||
class DropRateCategory(Choice):
|
class DropRateCategory(Choice):
|
||||||
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
|
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
|
||||||
Tiered: Based on item rarity/value
|
Tiered: Based on item rarity/value
|
||||||
|
@ -271,7 +250,6 @@ class DropRateCategory(Choice):
|
||||||
option_randomized = 2
|
option_randomized = 2
|
||||||
option_fixed = 3
|
option_fixed = 3
|
||||||
|
|
||||||
|
|
||||||
class FixedDropRate(Range):
|
class FixedDropRate(Range):
|
||||||
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
|
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
|
||||||
display_name = "Fixed Drop Rate"
|
display_name = "Fixed Drop Rate"
|
||||||
|
@ -279,7 +257,6 @@ class FixedDropRate(Range):
|
||||||
range_end = 100
|
range_end = 100
|
||||||
default = 5
|
default = 5
|
||||||
|
|
||||||
|
|
||||||
class LootTierDistro(Choice):
|
class LootTierDistro(Choice):
|
||||||
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
|
"""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
|
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_full_random = 1
|
||||||
option_inverted_weight = 2
|
option_inverted_weight = 2
|
||||||
|
|
||||||
|
|
||||||
class ShowBestiary(Toggle):
|
class ShowBestiary(Toggle):
|
||||||
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
|
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
|
||||||
display_name = "Show Bestiary Entries"
|
display_name = "Show Bestiary Entries"
|
||||||
|
|
||||||
|
|
||||||
class ShowDrops(Toggle):
|
class ShowDrops(Toggle):
|
||||||
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
|
"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"
|
display_name = "Show Bestiary Item Drops"
|
||||||
|
|
||||||
|
|
||||||
class EnterSandman(Toggle):
|
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"
|
"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"
|
display_name = "Enter Sandman"
|
||||||
|
|
||||||
|
|
||||||
class DadPercent(Toggle):
|
class DadPercent(Toggle):
|
||||||
"""The win condition is beating the boss of Emperor's Tower"""
|
"""The win condition is beating the boss of Emperor's Tower"""
|
||||||
display_name = "Dad Percent"
|
display_name = "Dad Percent"
|
||||||
|
|
||||||
|
|
||||||
class RisingTides(Toggle):
|
class RisingTides(Toggle):
|
||||||
"""Random areas are flooded or drained, can be further specified with RisingTidesOverrides"""
|
"""Random areas are flooded or drained, can be further specified with RisingTidesOverrides"""
|
||||||
display_name = "Rising Tides"
|
display_name = "Rising Tides"
|
||||||
|
|
||||||
|
|
||||||
def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]:
|
def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]:
|
||||||
if with_save_point_option:
|
if with_save_point_option:
|
||||||
return {
|
return {
|
||||||
|
@ -341,7 +312,6 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D
|
||||||
"Flooded")
|
"Flooded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RisingTidesOverrides(OptionDict):
|
class RisingTidesOverrides(OptionDict):
|
||||||
"""Odds for specific areas to be flooded or drained, only has effect when RisingTides is on.
|
"""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"""
|
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 },
|
"Lab": { "Dry": 67, "Flooded": 33 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UnchainedKeys(Toggle):
|
class UnchainedKeys(Toggle):
|
||||||
"""Start with Twin Pyramid Key, which does not give free warp;
|
"""Start with Twin Pyramid Key, which does not give free warp;
|
||||||
warp items for Past, Present, (and ??? with Enter Sandman) can be found."""
|
warp items for Past, Present, (and ??? with Enter Sandman) can be found."""
|
||||||
display_name = "Unchained Keys"
|
display_name = "Unchained Keys"
|
||||||
|
|
||||||
|
|
||||||
class TrapChance(Range):
|
class TrapChance(Range):
|
||||||
"""Chance of traps in the item pool.
|
"""Chance of traps in the item pool.
|
||||||
Traps will only replace filler items such as potions, vials and antidotes"""
|
Traps will only replace filler items such as potions, vials and antidotes"""
|
||||||
|
@ -388,67 +356,256 @@ class TrapChance(Range):
|
||||||
range_end = 100
|
range_end = 100
|
||||||
default = 10
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
class Traps(OptionList):
|
class Traps(OptionList):
|
||||||
"""List of traps that may be in the item pool to find"""
|
"""List of traps that may be in the item pool to find"""
|
||||||
display_name = "Traps Types"
|
display_name = "Traps Types"
|
||||||
valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" }
|
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" ]
|
default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ]
|
||||||
|
|
||||||
|
|
||||||
class PresentAccessWithWheelAndSpindle(Toggle):
|
class PresentAccessWithWheelAndSpindle(Toggle):
|
||||||
"""When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired."""
|
"""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
|
class HiddenDamageRandoOverrides(DamageRandoOverrides):
|
||||||
timespinner_options: Dict[str, Option] = {
|
"""Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that
|
||||||
"StartWithJewelryBox": StartWithJewelryBox,
|
you don't specify will roll with 1/1/1 as odds"""
|
||||||
"DownloadableItems": DownloadableItems,
|
visibility = Visibility.none
|
||||||
"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 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:
|
class HiddenTraps(Traps):
|
||||||
return get_option_value(world, player, name) > 0
|
"""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
|
||||||
|
|
||||||
|
class HasReplacedCamelCase(Toggle):
|
||||||
|
"""For internal use will display a warning message if true"""
|
||||||
|
visibility = Visibility.none
|
||||||
|
|
||||||
def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]:
|
@dataclass
|
||||||
option = getattr(world, name, None)
|
class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
|
||||||
if option == None:
|
StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore
|
||||||
return 0
|
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
|
||||||
|
|
||||||
return option[player].value
|
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 typing import Tuple, Dict, Union, List
|
||||||
from BaseClasses import MultiWorld
|
from random import Random
|
||||||
from .Options import timespinner_options, is_option_enabled, get_option_value
|
from .Options import TimespinnerOptions
|
||||||
|
|
||||||
class PreCalculatedWeights:
|
class PreCalculatedWeights:
|
||||||
pyramid_keys_unlock: str
|
pyramid_keys_unlock: str
|
||||||
|
@ -21,22 +21,22 @@ class PreCalculatedWeights:
|
||||||
flood_lake_serene_bridge: bool
|
flood_lake_serene_bridge: bool
|
||||||
flood_lab: bool
|
flood_lab: bool
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, options: TimespinnerOptions, random: Random):
|
||||||
if world and is_option_enabled(world, player, "RisingTides"):
|
if options.rising_tides:
|
||||||
weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player)
|
weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options)
|
||||||
|
|
||||||
self.flood_basement, self.flood_basement_high = \
|
self.flood_basement, self.flood_basement_high = \
|
||||||
self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement")
|
self.roll_flood_setting(random, weights_overrrides, "CastleBasement")
|
||||||
self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion")
|
self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion")
|
||||||
self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw")
|
self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw")
|
||||||
self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft")
|
self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft")
|
||||||
self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman")
|
self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman")
|
||||||
self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat")
|
self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat")
|
||||||
self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard")
|
self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard")
|
||||||
self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation")
|
self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation")
|
||||||
self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene")
|
self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene")
|
||||||
self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge")
|
self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge")
|
||||||
self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab")
|
self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab")
|
||||||
else:
|
else:
|
||||||
self.flood_basement = False
|
self.flood_basement = False
|
||||||
self.flood_basement_high = False
|
self.flood_basement_high = False
|
||||||
|
@ -52,10 +52,12 @@ class PreCalculatedWeights:
|
||||||
self.flood_lab = False
|
self.flood_lab = False
|
||||||
|
|
||||||
self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \
|
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
|
@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] = [
|
present_teleportation_gates: List[str] = [
|
||||||
"GateKittyBoss",
|
"GateKittyBoss",
|
||||||
"GateLeftLibrary",
|
"GateLeftLibrary",
|
||||||
|
@ -80,38 +82,30 @@ class PreCalculatedWeights:
|
||||||
"GateRightPyramid"
|
"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:
|
if not is_maw_flooded:
|
||||||
past_teleportation_gates.append("GateMaw")
|
past_teleportation_gates.append("GateMaw")
|
||||||
|
|
||||||
if not is_xarion_flooded:
|
if not is_xarion_flooded:
|
||||||
present_teleportation_gates.append("GateXarion")
|
present_teleportation_gates.append("GateXarion")
|
||||||
|
|
||||||
if is_option_enabled(world, player, "Inverted"):
|
if options.inverted:
|
||||||
all_gates: Tuple[str, ...] = present_teleportation_gates
|
all_gates: Tuple[str, ...] = present_teleportation_gates
|
||||||
else:
|
else:
|
||||||
all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates
|
all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates
|
||||||
|
|
||||||
return (
|
return (
|
||||||
world.random.choice(all_gates),
|
random.choice(all_gates),
|
||||||
world.random.choice(present_teleportation_gates),
|
random.choice(present_teleportation_gates),
|
||||||
world.random.choice(past_teleportation_gates),
|
random.choice(past_teleportation_gates),
|
||||||
world.random.choice(ancient_pyramid_teleportation_gates)
|
random.choice(ancient_pyramid_teleportation_gates)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@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]]]] = \
|
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:
|
if not weights_overrides_option:
|
||||||
weights_overrides_option = default_weights
|
weights_overrides_option = default_weights
|
||||||
|
@ -123,13 +117,13 @@ class PreCalculatedWeights:
|
||||||
return weights_overrides_option
|
return weights_overrides_option
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def roll_flood_setting(world: MultiWorld, player: int,
|
def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]],
|
||||||
all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]:
|
key: str) -> Tuple[bool, bool]:
|
||||||
|
|
||||||
weights: Union[Dict[str, int], str] = all_weights[key]
|
weights: Union[Dict[str, int], str] = all_weights[key]
|
||||||
|
|
||||||
if isinstance(weights, dict):
|
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:
|
else:
|
||||||
result: str = weights
|
result: str = weights
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from typing import List, Set, Dict, Optional, Callable
|
from typing import List, Set, Dict, Optional, Callable
|
||||||
from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location
|
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 .Locations import LocationData, get_location_datas
|
||||||
from .PreCalculatedWeights import PreCalculatedWeights
|
from .PreCalculatedWeights import PreCalculatedWeights
|
||||||
from .LogicExtensions import TimespinnerLogic
|
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(
|
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 = [
|
regions = [
|
||||||
create_region(world, player, locations_per_region, 'Menu'),
|
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')
|
create_region(world, player, locations_per_region, 'Space time continuum')
|
||||||
]
|
]
|
||||||
|
|
||||||
if is_option_enabled(world, player, "GyreArchives"):
|
if options.gyre_archives:
|
||||||
regions.extend([
|
regions.extend([
|
||||||
create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'),
|
create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'),
|
||||||
create_region(world, player, locations_per_region, 'Ifrit\'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
|
world.regions += regions
|
||||||
|
|
||||||
connectStartingRegion(world, player)
|
connectStartingRegion(world, player, options)
|
||||||
|
|
||||||
flooded: PreCalculatedWeights = precalculated_weights
|
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', '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")
|
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)', 'Skeleton Shaft')
|
||||||
connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport)
|
connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport)
|
||||||
connect(world, player, 'Refugee Camp', 'Forest')
|
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, 'Refugee Camp', 'Space time continuum', logic.has_teleport)
|
||||||
connect(world, player, 'Forest', 'Refugee Camp')
|
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))
|
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', '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 (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', '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 (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"))
|
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, '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, '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")
|
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
|
return region
|
||||||
|
|
||||||
|
|
||||||
def connectStartingRegion(world: MultiWorld, player: int):
|
def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions):
|
||||||
menu = world.get_region('Menu', player)
|
menu = world.get_region('Menu', player)
|
||||||
tutorial = world.get_region('Tutorial', player)
|
tutorial = world.get_region('Tutorial', player)
|
||||||
space_time_continuum = world.get_region('Space time continuum', 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)
|
starting_region = world.get_region('Refugee Camp', player)
|
||||||
else:
|
else:
|
||||||
starting_region = world.get_region('Lake desolation', player)
|
starting_region = world.get_region('Lake desolation', player)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
from typing import Dict, List, Set, Tuple, TextIO, Union
|
from typing import Dict, List, Set, Tuple, TextIO
|
||||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
from BaseClasses import Item, Tutorial, ItemClassification
|
||||||
from .Items import get_item_names_per_category
|
from .Items import get_item_names_per_category
|
||||||
from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items
|
from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items
|
||||||
from .Locations import get_location_datas, EventId
|
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 .PreCalculatedWeights import PreCalculatedWeights
|
||||||
from .Regions import create_regions_and_locations
|
from .Regions import create_regions_and_locations
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
import logging
|
||||||
|
|
||||||
class TimespinnerWebWorld(WebWorld):
|
class TimespinnerWebWorld(WebWorld):
|
||||||
theme = "ice"
|
theme = "ice"
|
||||||
|
@ -35,32 +36,34 @@ class TimespinnerWorld(World):
|
||||||
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
|
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.
|
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 = BackwardsCompatiableTimespinnerOptions
|
||||||
option_definitions = timespinner_options
|
options: BackwardsCompatiableTimespinnerOptions
|
||||||
game = "Timespinner"
|
game = "Timespinner"
|
||||||
topology_present = True
|
topology_present = True
|
||||||
web = TimespinnerWebWorld()
|
web = TimespinnerWebWorld()
|
||||||
required_client_version = (0, 4, 2)
|
required_client_version = (0, 4, 2)
|
||||||
|
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
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()
|
item_name_groups = get_item_names_per_category()
|
||||||
|
|
||||||
precalculated_weights: PreCalculatedWeights
|
precalculated_weights: PreCalculatedWeights
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
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
|
# 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:
|
if self.options.start_inventory.value.pop('Meyef', 0) > 0:
|
||||||
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
|
self.options.start_with_meyef.value = Toggle.option_true
|
||||||
if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0:
|
if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0:
|
||||||
self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true
|
self.options.quick_seed.value = Toggle.option_true
|
||||||
if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
|
if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0:
|
||||||
self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true
|
self.options.start_with_jewelry_box.value = Toggle.option_true
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
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:
|
def create_items(self) -> None:
|
||||||
self.create_and_assign_event_items()
|
self.create_and_assign_event_items()
|
||||||
|
@ -74,7 +77,7 @@ class TimespinnerWorld(World):
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
final_boss: str
|
final_boss: str
|
||||||
if self.is_option_enabled("DadPercent"):
|
if self.options.dad_percent:
|
||||||
final_boss = "Killed Emperor"
|
final_boss = "Killed Emperor"
|
||||||
else:
|
else:
|
||||||
final_boss = "Killed Nightmare"
|
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)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player)
|
||||||
|
|
||||||
def fill_slot_data(self) -> Dict[str, object]:
|
def fill_slot_data(self) -> Dict[str, object]:
|
||||||
slot_data: Dict[str, object] = {}
|
return {
|
||||||
|
# options
|
||||||
ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"}
|
"StartWithJewelryBox": self.options.start_with_jewelry_box.value,
|
||||||
|
"DownloadableItems": self.options.downloadable_items.value,
|
||||||
for option_name in timespinner_options:
|
"EyeSpy": self.options.eye_spy.value,
|
||||||
if (option_name not in ap_specific_settings):
|
"StartWithMeyef": self.options.start_with_meyef.value,
|
||||||
slot_data[option_name] = self.get_option_value(option_name)
|
"QuickSeed": self.options.quick_seed.value,
|
||||||
|
"SpecificKeycards": self.options.specific_keycards.value,
|
||||||
slot_data["StinkyMaw"] = True
|
"Inverted": self.options.inverted.value,
|
||||||
slot_data["ProgressiveVerticalMovement"] = False
|
"GyreArchives": self.options.gyre_archives.value,
|
||||||
slot_data["ProgressiveKeycards"] = False
|
"Cantoran": self.options.cantoran.value,
|
||||||
slot_data["PersonalItems"] = self.get_personal_items()
|
"LoreChecks": self.options.lore_checks.value,
|
||||||
slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock
|
"BossRando": self.options.boss_rando.value,
|
||||||
slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock
|
"DamageRando": self.options.damage_rando.value,
|
||||||
slot_data["PastGate"] = self.precalculated_weights.past_key_unlock
|
"DamageRandoOverrides": self.options.damage_rando_overrides.value,
|
||||||
slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock
|
"HpCap": self.options.hp_cap.value,
|
||||||
slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \
|
"LevelCap": self.options.level_cap.value,
|
||||||
int(self.precalculated_weights.flood_basement_high)
|
"ExtraEarringsXP": self.options.extra_earrings_xp.value,
|
||||||
slot_data["Xarion"] = self.precalculated_weights.flood_xarion
|
"BossHealing": self.options.boss_healing.value,
|
||||||
slot_data["Maw"] = self.precalculated_weights.flood_maw
|
"ShopFill": self.options.shop_fill.value,
|
||||||
slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft
|
"ShopWarpShards": self.options.shop_warp_shards.value,
|
||||||
slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back
|
"ShopMultiplier": self.options.shop_multiplier.value,
|
||||||
slot_data["CastleMoat"] = self.precalculated_weights.flood_moat
|
"LootPool": self.options.loot_pool.value,
|
||||||
slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard
|
"DropRateCategory": self.options.drop_rate_category.value,
|
||||||
slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation
|
"FixedDropRate": self.options.fixed_drop_rate.value,
|
||||||
slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene
|
"LootTierDistro": self.options.loot_tier_distro.value,
|
||||||
slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge
|
"ShowBestiary": self.options.show_bestiary.value,
|
||||||
slot_data["Lab"] = self.precalculated_weights.flood_lab
|
"ShowDrops": self.options.show_drops.value,
|
||||||
|
"EnterSandman": self.options.enter_sandman.value,
|
||||||
return slot_data
|
"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:
|
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'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')
|
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')
|
spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n')
|
||||||
else:
|
else:
|
||||||
spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n')
|
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] = []
|
flooded_areas: List[str] = []
|
||||||
|
|
||||||
if self.precalculated_weights.flood_basement:
|
if self.precalculated_weights.flood_basement:
|
||||||
|
@ -159,6 +188,15 @@ class TimespinnerWorld(World):
|
||||||
|
|
||||||
spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n')
|
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:
|
def create_item(self, name: str) -> Item:
|
||||||
data = item_table[name]
|
data = item_table[name]
|
||||||
|
|
||||||
|
@ -176,41 +214,41 @@ class TimespinnerWorld(World):
|
||||||
if not item.advancement:
|
if not item.advancement:
|
||||||
return item
|
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
|
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
|
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
|
item.classification = ItemClassification.filler
|
||||||
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
|
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
|
item.classification = ItemClassification.filler
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
trap_chance: int = self.get_option_value("TrapChance")
|
trap_chance: int = self.options.trap_chance.value
|
||||||
enabled_traps: List[str] = self.get_option_value("Traps")
|
enabled_traps: List[str] = self.options.traps.value
|
||||||
|
|
||||||
if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps:
|
if self.random.random() < (trap_chance / 100) and enabled_traps:
|
||||||
return self.multiworld.random.choice(enabled_traps)
|
return self.random.choice(enabled_traps)
|
||||||
else:
|
else:
|
||||||
return self.multiworld.random.choice(filler_items)
|
return self.random.choice(filler_items)
|
||||||
|
|
||||||
def get_excluded_items(self) -> Set[str]:
|
def get_excluded_items(self) -> Set[str]:
|
||||||
excluded_items: Set[str] = set()
|
excluded_items: Set[str] = set()
|
||||||
|
|
||||||
if self.is_option_enabled("StartWithJewelryBox"):
|
if self.options.start_with_jewelry_box:
|
||||||
excluded_items.add('Jewelry Box')
|
excluded_items.add('Jewelry Box')
|
||||||
if self.is_option_enabled("StartWithMeyef"):
|
if self.options.start_with_meyef:
|
||||||
excluded_items.add('Meyef')
|
excluded_items.add('Meyef')
|
||||||
if self.is_option_enabled("QuickSeed"):
|
if self.options.quick_seed:
|
||||||
excluded_items.add('Talaria Attachment')
|
excluded_items.add('Talaria Attachment')
|
||||||
|
|
||||||
if self.is_option_enabled("UnchainedKeys"):
|
if self.options.unchained_keys:
|
||||||
excluded_items.add('Twin Pyramid Key')
|
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')
|
excluded_items.add('Mysterious Warp Beacon')
|
||||||
else:
|
else:
|
||||||
excluded_items.add('Timeworn Warp Beacon')
|
excluded_items.add('Timeworn Warp Beacon')
|
||||||
|
@ -224,8 +262,8 @@ class TimespinnerWorld(World):
|
||||||
return excluded_items
|
return excluded_items
|
||||||
|
|
||||||
def assign_starter_items(self, excluded_items: Set[str]) -> None:
|
def assign_starter_items(self, excluded_items: Set[str]) -> None:
|
||||||
non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value
|
non_local_items: Set[str] = self.options.non_local_items.value
|
||||||
local_items: Set[str] = self.multiworld.local_items[self.player].value
|
local_items: Set[str] = self.options.local_items.value
|
||||||
|
|
||||||
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if
|
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if
|
||||||
item in local_items or not item in non_local_items)
|
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)
|
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:
|
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)
|
self.place_locked_item(excluded_items, location, item_name)
|
||||||
|
|
||||||
def place_first_progression_item(self, excluded_items: Set[str]) -> None:
|
def place_first_progression_item(self, excluded_items: Set[str]) -> None:
|
||||||
if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \
|
if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation:
|
||||||
or self.precalculated_weights.flood_lake_desolation:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for item in self.multiworld.precollected_items[self.player]:
|
for item_name in self.options.start_inventory.value.keys():
|
||||||
if item.name in starter_progression_items and not item.name in excluded_items:
|
if item_name in starter_progression_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
local_starter_progression_items = tuple(
|
local_starter_progression_items = tuple(
|
||||||
item for item in starter_progression_items
|
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:
|
if not local_starter_progression_items:
|
||||||
return
|
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
|
self.multiworld.local_early_items[self.player][progression_item] = 1
|
||||||
|
|
||||||
|
@ -307,9 +344,3 @@ class TimespinnerWorld(World):
|
||||||
personal_items[location.address] = location.item.code
|
personal_items[location.address] = location.item.code
|
||||||
|
|
||||||
return personal_items
|
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