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