Timespinner: migrate to new options api and correct random ()

* 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:
Jarno 2024-07-31 11:50:04 +02:00 committed by GitHub
parent 77e3f9fbef
commit 1d19da0c76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 417 additions and 233 deletions

View File

@ -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),

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)