diff --git a/README.md b/README.md index 06a04b13..c694adb0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Currently, the following games are supported: * Slay the Spire * Risk of Rain 2 * The Legend of Zelda: Ocarina of Time +* Timespinner For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/WebHostLib/static/assets/gameInfo/en_Timespinner.md b/WebHostLib/static/assets/gameInfo/en_Timespinner.md new file mode 100644 index 00000000..1b0eb0d4 --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_Timespinner.md @@ -0,0 +1,28 @@ +# Timespinner + +## Where is the settings page? +The player settings page for this game is located here. It contains all the options +you need to configure and export a config file. + +## What does randomization do to this game? +Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game +is always able to be completed, but because of the item shuffle the player may need to access certain areas before +they would in the vanilla game. All rings and spells are also randomized into those item locations, therefor you can nolonger craft them at the alchemist + +## What is the goal of Timespinner when randomized? +The goal remains unchanged. Kill the Sandman\Nightmare! + +## What items and locations get shuffled? +All main inventory items, orbs, collectables, and familiers can be shuffled, and all locations in the game which could +contain any of those items may have their contents changed. + +## Which items can be in another player's world? +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to +limit certain items to your own world. + +## What does another world's item look like in Subnautica? +Items belonging to other worlds are represented by the vanilla item [Elemental Beads](https://timespinnerwiki.com/Use_Items), Elemental Beads have no use in the randomizer + +## When the player receives an item, what happens? +When the player receives an item, the same items popup will be displayed as when you would normally obtain the item + diff --git a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md new file mode 100644 index 00000000..9dec349b --- /dev/null +++ b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md @@ -0,0 +1,60 @@ +# Timespinner Randomizer Setup Guide + +## Required Software + +- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner) +- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) + +## General Concept + +The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items + +## Installation Procedures + +Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the [readme](https://github.com/JarnoWesthof/TsRandomizer) + +## Joining a MultiWorld Game + +1. Run TsRandomizer.exe +2. Select "New Game" +3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard +4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails + * NOTE: the input fields support Ctrl + V pasting of values +5. Select "Connect" +6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty + +## YAML Settings +An example YAML would look like this: +```yaml +description: Default Timespinner Template +name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit +game: + Timespinner: 1 +requires: + version: 0.1.8 +Timespinner: + StartWithJewelryBox: # Start with Jewelry Box unlocked + false: 50 + true: 0 + DownloadableItems: # With the tablet you will be able to download items at terminals + false: 50 + true: 50 + FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors + false: 50 + true: 0 + StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer + false: 50 + true: 50 + QuickSeed: # Start with Talaria Attachment, Nyoom! + false: 50 + true: 0 + SpecificKeycards: # Keycards can only open corresponding doors + false: 0 + true: 50 + Inverted: # Start in the past + false: 50 + true: 50 +``` +* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight +* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds +* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds \ No newline at end of file diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 4931e4b2..b477ea94 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -158,5 +158,24 @@ ] } ] + }, + { + "gameTitle": "Timespinner", + "tutorials": [ + { + "name": "Multiworld Setup Guide", + "description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld", + "files": [ + { + "language": "English", + "filename": "timespinner/setup_en.md", + "link": "timespinner/setup/en", + "authors": [ + "Jarno" + ] + } + ] + } + ] } ] diff --git a/docs/network.png b/docs/network.png index 223a9890..7eb996e2 100644 Binary files a/docs/network.png and b/docs/network.png differ diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 1af2aa0e..04d8c06a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -81,7 +81,7 @@ class World(metaclass=AutoWorldRegister): # increment this every time something in your world's names/id mappings changes. # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be # retrieved by clients on every connection. - data_version = 1 + data_version: int = 1 hint_blacklist: Set[str] = frozenset() # any names that should not be hintable @@ -100,7 +100,7 @@ class World(metaclass=AutoWorldRegister): forced_auto_forfeit: bool = False # Hide World Type from various views. Does not remove functionality. - hidden = False + hidden: bool = False # autoset on creation: world: MultiWorld diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index 4f45b658..8641cb9a 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -1,4 +1,4 @@ -from typing import Dict, Tuple, NamedTuple +from typing import Dict, Set, Tuple, NamedTuple class ItemData(NamedTuple): category: str @@ -9,50 +9,50 @@ class ItemData(NamedTuple): # A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired item_table: Dict[str, ItemData] = { 'Eternal Crown': ItemData('Equipment', 1337000), - #'Security Visor': ItemData('Equipment', 1337001), - #'Engineer Goggles': ItemData('Equipment', 1337002), - #'Leather Helmet': ItemData('Equipment', 1337003), - #'Copper Helmet': ItemData('Equipment', 1337004), + 'Security Visor': ItemData('Equipment', 1337001, 0), + 'Engineer Goggles': ItemData('Equipment', 1337002, 0), + 'Leather Helmet': ItemData('Equipment', 1337003, 0), + 'Copper Helmet': ItemData('Equipment', 1337004, 0), 'Pointy Hat': ItemData('Equipment', 1337005), - #'Dragoon Helmet': ItemData('Equipment', 1337006), + 'Dragoon Helmet': ItemData('Equipment', 1337006, 0), 'Buckle Hat': ItemData('Equipment', 1337007), - #'Advisor Hat': ItemData('Equipment', 1337008), + 'Advisor Hat': ItemData('Equipment', 1337008, 0), 'Librarian Hat': ItemData('Equipment', 1337009), - #'Combat Helmet': ItemData('Equipment', 1337010), + 'Combat Helmet': ItemData('Equipment', 1337010, 0), 'Captain\'s Cap': ItemData('Equipment', 1337011), 'Lab Glasses': ItemData('Equipment', 1337012), 'Empire Crown': ItemData('Equipment', 1337013), 'Viletian Crown': ItemData('Equipment', 1337014), - #'Sunglasses': ItemData('Equipment', 1337015), + 'Sunglasses': ItemData('Equipment', 1337015, 0), 'Old Coat': ItemData('Equipment', 1337016), - #'Trendy Jacket': ItemData('Equipment', 1337017), - #'Security Vest': ItemData('Equipment', 1337018), - #'Leather Jerkin': ItemData('Equipment', 1337019), - #'Copper Breastplate': ItemData('Equipment', 1337020), + 'Trendy Jacket': ItemData('Equipment', 1337017, 0), + 'Security Vest': ItemData('Equipment', 1337018, 0), + 'Leather Jerkin': ItemData('Equipment', 1337019, 0), + 'Copper Breastplate': ItemData('Equipment', 1337020, 0), 'Traveler\'s Cloak': ItemData('Equipment', 1337021), - #'Dragoon Armor': ItemData('Equipment', 1337022), + 'Dragoon Armor': ItemData('Equipment', 1337022, 0), 'Midnight Cloak': ItemData('Equipment', 1337023), - #'Advisor Robe': ItemData('Equipment', 1337024), + 'Advisor Robe': ItemData('Equipment', 1337024, 0), 'Librarian Robe': ItemData('Equipment', 1337025), - #'Military Armor': ItemData('Equipment', 1337026), + 'Military Armor': ItemData('Equipment', 1337026, 0), 'Captain\'s Uniform': ItemData('Equipment', 1337027), 'Lab Coat': ItemData('Equipment', 1337028), 'Empress Robe': ItemData('Equipment', 1337029), 'Princess Dress': ItemData('Equipment', 1337030), 'Eternal Coat': ItemData('Equipment', 1337031), - #'Synthetic Plume': ItemData('Equipment', 1337032), - #'Cheveur Plume': ItemData('Equipment', 1337033), + 'Synthetic Plume': ItemData('Equipment', 1337032, 0), + 'Cheveur Plume': ItemData('Equipment', 1337033, 0), 'Metal Wristband': ItemData('Equipment', 1337034), - #'Nymph Hairband': ItemData('Equipment', 1337035), - #'Mother o\' Pearl': ItemData('Equipment', 1337036), + 'Nymph Hairband': ItemData('Equipment', 1337035, 0), + 'Mother o\' Pearl': ItemData('Equipment', 1337036, 0), 'Bird Statue': ItemData('Equipment', 1337037), - #'Chaos Stole': ItemData('Equipment', 1337038), + 'Chaos Stole': ItemData('Equipment', 1337038, 0), 'Pendulum': ItemData('Equipment', 1337039), - #'Chaos Horn': ItemData('Equipment', 1337040), + 'Chaos Horn': ItemData('Equipment', 1337040, 0), 'Filigree Clasp': ItemData('Equipment', 1337041), - #'Azure Stole': ItemData('Equipment', 1337042), + 'Azure Stole': ItemData('Equipment', 1337042, 0), 'Ancient Coin': ItemData('Equipment', 1337043), - #'Shiny Rock': ItemData('Equipment', 1337044), + 'Shiny Rock': ItemData('Equipment', 1337044, 0), 'Galaxy Earrings': ItemData('Equipment', 1337045), 'Selen\'s Bangle': ItemData('Equipment', 1337046), 'Glass Pumpkin': ItemData('Equipment', 1337047), @@ -76,45 +76,45 @@ item_table: Dict[str, ItemData] = { 'Antidote': ItemData('UseItem', 1337065, 0), 'Chaos Rose': ItemData('UseItem', 1337066, 0), 'Warp Shard': ItemData('UseItem', 1337067), - #'Dream Wisp': ItemData('UseItem', 1337068), - #'PlaceHolderItem1': ItemData('UseItem', 1337069), - #'Lachiemi Sun': ItemData('UseItem', 1337070), + 'Dream Wisp': ItemData('UseItem', 1337068, 0), + 'PlaceHolderItem1': ItemData('UseItem', 1337069, 0), + 'Lachiemi Sun': ItemData('UseItem', 1337070, 0), 'Jerky': ItemData('UseItem', 1337071), - #'Biscuit': ItemData('UseItem', 1337072), - #'Fried Cheveur': ItemData('UseItem', 1337073), - #'Sautéed Wyvern Tail': ItemData('UseItem', 1337074), - #'Unagi Roll': ItemData('UseItem', 1337075), - #'Cheveur au Vin': ItemData('UseItem', 1337076), - #'Royal Casserole': ItemData('UseItem', 1337077), + 'Biscuit': ItemData('UseItem', 1337072, 0), + 'Fried Cheveur': ItemData('UseItem', 1337073, 0), + 'Sautéed Wyvern Tail': ItemData('UseItem', 1337074, 0), + 'Unagi Roll': ItemData('UseItem', 1337075, 0), + 'Cheveur au Vin': ItemData('UseItem', 1337076, 0), + 'Royal Casserole': ItemData('UseItem', 1337077, 0), 'Spaghetti': ItemData('UseItem', 1337078), - #'Plump Maggot': ItemData('UseItem', 1337079), - #'Orange Juice': ItemData('UseItem', 1337080), + 'Plump Maggot': ItemData('UseItem', 1337079, 0), + 'Orange Juice': ItemData('UseItem', 1337080, 0), 'Filigree Tea': ItemData('UseItem', 1337081), - #'Empress Cake': ItemData('UseItem', 1337082), - #'Rotten Tail': ItemData('UseItem', 1337083), - #'Alchemy Tools': ItemData('UseItem', 1337084), + 'Empress Cake': ItemData('UseItem', 1337082, 0), + 'Rotten Tail': ItemData('UseItem', 1337083, 0), + 'Alchemy Tools': ItemData('UseItem', 1337084, 0), 'Galaxy Stone': ItemData('UseItem', 1337085), - #1337086 Used interally - #'Essence Crystal': ItemData('UseItem', 1337087), - #'Gold Ring': ItemData('UseItem', 1337088), - #'Gold Necklace': ItemData('UseItem', 1337089), + # 1337086 Used interally + 'Essence Crystal': ItemData('UseItem', 1337087, 0), + 'Gold Ring': ItemData('UseItem', 1337088, 0), + 'Gold Necklace': ItemData('UseItem', 1337089, 0), 'Herb': ItemData('UseItem', 1337090), - #'Mushroom': ItemData('UseItem', 1337091), - #'Plasma Crystal': ItemData('UseItem', 1337092), + 'Mushroom': ItemData('UseItem', 1337091, 0), + 'Plasma Crystal': ItemData('UseItem', 1337092, 0), 'Plasma IV Bag': ItemData('UseItem', 1337093), - #'Cheveur Drumstick': ItemData('UseItem', 1337094), - #'Wyvern Tail': ItemData('UseItem', 1337095), - #'Eel Meat': ItemData('UseItem', 1337096), - #'Cheveux Breast': ItemData('UseItem', 1337097), + 'Cheveur Drumstick': ItemData('UseItem', 1337094, 0), + 'Wyvern Tail': ItemData('UseItem', 1337095, 0), + 'Eel Meat': ItemData('UseItem', 1337096, 0), + 'Cheveux Breast': ItemData('UseItem', 1337097, 0), 'Food Synthesizer': ItemData('UseItem', 1337098), - #'Cheveux Feather': ItemData('UseItem', 1337099), - #'Siren Ink': ItemData('UseItem', 1337100), - #'Plasma Core': ItemData('UseItem', 1337101), - #'Silver Ore': ItemData('UseItem', 1337102), - #'Historical Documents': ItemData('UseItem', 1337103), - #'MapReveal 0': ItemData('UseItem', 1337104), - #'MapReveal 1': ItemData('UseItem', 1337105), - #'MapReveal 2': ItemData('UseItem', 1337106), + 'Cheveux Feather': ItemData('UseItem', 1337099, 0), + 'Siren Ink': ItemData('UseItem', 1337100, 0), + 'Plasma Core': ItemData('UseItem', 1337101, 0), + 'Silver Ore': ItemData('UseItem', 1337102, 0), + 'Historical Documents': ItemData('UseItem', 1337103, 0), + 'MapReveal 0': ItemData('UseItem', 1337104, 0), + 'MapReveal 1': ItemData('UseItem', 1337105, 0), + 'MapReveal 2': ItemData('UseItem', 1337106, 0), 'Timespinner Wheel': ItemData('Relic', 1337107, progression=True), 'Timespinner Spindle': ItemData('Relic', 1337108, progression=True), 'Timespinner Gear 1': ItemData('Relic', 1337109, progression=True), @@ -193,7 +193,7 @@ item_table: Dict[str, ItemData] = { 'Max Sand': ItemData('Stat', 1337249, 14) } -starter_melee_weapons: Tuple[str] = ( +starter_melee_weapons: Tuple[str, ...] = ( 'Blue Orb', 'Blade Orb', 'Fire Orb', @@ -211,7 +211,7 @@ starter_melee_weapons: Tuple[str] = ( 'Radiant Orb' ) -starter_spells: Tuple[str] = ( +starter_spells: Tuple[str, ...] = ( 'Colossal Blade', 'Infernal Flames', 'Plasma Geyser', @@ -229,7 +229,7 @@ starter_spells: Tuple[str] = ( ) # weighted -starter_progression_items: Tuple[str] = ( +starter_progression_items: Tuple[str, ...] = ( 'Talaria Attachment', 'Talaria Attachment', 'Succubus Hairpin', @@ -241,7 +241,7 @@ starter_progression_items: Tuple[str] = ( 'Lightwall' ) -filler_items: Tuple[str] = ( +filler_items: Tuple[str, ...] = ( 'Potion', 'Ether', 'Hi-Potion', @@ -254,4 +254,12 @@ filler_items: Tuple[str] = ( 'Mind Refresh ULTRA', 'Antidote', 'Chaos Rose' -) \ No newline at end of file +) + +def get_item_names_per_category() -> Dict[str, Set[str]]: + categories: Dict[str, Set[str]] = {} + + for name, data in item_table.items(): + categories.setdefault(data.category, set()).add(name) + + return categories \ No newline at end of file diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 699c2111..7e0a884d 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -4,14 +4,12 @@ from .Options import is_option_enabled EventId: Optional[int] = None - class LocationData(NamedTuple): region: str name: str code: Optional[int] rule: Callable = lambda state: True - def get_locations(world: Optional[MultiWorld], player: Optional[int]): location_table: Tuple[LocationData, ...] = ( # PresentItemLocations @@ -200,6 +198,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]): # DownloadTerminals LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)), LocationData('Libary', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)), + # 1337158 Is Lost in time LocationData('Libary', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)), LocationData('Libary', 'V terminal 1', 1337160, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)), LocationData('Libary', 'V terminal 2', 1337161, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)), @@ -218,6 +217,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]): return ( *location_table, *downloadable_items ) else: return location_table + starter_progression_locations: Tuple[str, ...] = ( 'Starter chest 2', diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 3ee636cc..8d179ac7 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -19,7 +19,7 @@ class DownloadableItems(Toggle): display_name = "Downloadable items" class FacebookMode(Toggle): - "With the tablet you will be able to download items at terminals" + "Requires Oculus Rift(ng) to spot the weakspots in walls and floors" display_name = "Facebook mode" class StartWithMeyef(Toggle): diff --git a/worlds/timespinner/PyramidKeys.py b/worlds/timespinner/PyramidKeys.py index b9cb4368..761dccb9 100644 --- a/worlds/timespinner/PyramidKeys.py +++ b/worlds/timespinner/PyramidKeys.py @@ -3,7 +3,7 @@ from BaseClasses import MultiWorld from .Options import is_option_enabled def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str: - present_teleportation_gates: Tuple[str] = ( + present_teleportation_gates: Tuple[str, ...] = ( "GateKittyBoss", "GateLeftLibrary", "GateMilitairyGate", @@ -12,7 +12,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str: "GateLakeDesolation" ) - past_teleportation_gates: Tuple[str] = ( + past_teleportation_gates: Tuple[str, ...] = ( "GateLakeSirineRight", "GateAccessToPast", "GateCastleRamparts", diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index f3639927..1d02afcc 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -3,46 +3,46 @@ from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Options import is_option_enabled from .Locations import LocationData -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData], pyramid_keys_unlock: str): +def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str): locations_per_region = get_locations_per_region(locations) world.regions += [ - create_region(world, player, locations_per_region, 'Menu'), - create_region(world, player, locations_per_region, 'Tutorial'), - create_region(world, player, locations_per_region, 'Lake desolation'), - create_region(world, player, locations_per_region, 'Upper lake desolation'), - create_region(world, player, locations_per_region, 'Lower lake desolation'), - create_region(world, player, locations_per_region, 'Libary'), - create_region(world, player, locations_per_region, 'Libary top'), - create_region(world, player, locations_per_region, 'Varndagroth tower left'), - create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'), - create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'), - create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'), - create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'), - create_region(world, player, locations_per_region, 'Militairy Fortress'), - create_region(world, player, locations_per_region, 'The lab'), - create_region(world, player, locations_per_region, 'The lab (power off)'), - create_region(world, player, locations_per_region, 'The lab (upper)'), - create_region(world, player, locations_per_region, 'Emperors tower'), - create_region(world, player, locations_per_region, 'Skeleton Shaft'), - create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), - create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), - create_region(world, player, locations_per_region, 'Refugee Camp'), - create_region(world, player, locations_per_region, 'Forest'), - create_region(world, player, locations_per_region, 'Left Side forest Caves'), - create_region(world, player, locations_per_region, 'Upper Lake Sirine'), - create_region(world, player, locations_per_region, 'Lower Lake Sirine'), - create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'), - create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'), - create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'), - create_region(world, player, locations_per_region, 'Caste Ramparts'), - create_region(world, player, locations_per_region, 'Caste Keep'), - create_region(world, player, locations_per_region, 'Royal towers (lower)'), - create_region(world, player, locations_per_region, 'Royal towers'), - create_region(world, player, locations_per_region, 'Royal towers (upper)'), - create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'), - create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'), - create_region(world, player, locations_per_region, 'Space time continuum') + create_region(world, player, locations_per_region, location_cache, 'Menu'), + create_region(world, player, locations_per_region, location_cache, 'Tutorial'), + create_region(world, player, locations_per_region, location_cache, 'Lake desolation'), + create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'), + create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'), + create_region(world, player, locations_per_region, location_cache, 'Libary'), + create_region(world, player, locations_per_region, location_cache, 'Libary top'), + create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'), + create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'), + create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'), + create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'), + create_region(world, player, locations_per_region, location_cache, 'Militairy Fortress'), + create_region(world, player, locations_per_region, location_cache, 'The lab'), + create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'), + create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Emperors tower'), + create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'), + create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'), + create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'), + create_region(world, player, locations_per_region, location_cache, 'Forest'), + create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'), + create_region(world, player, locations_per_region, location_cache, 'Upper Lake Sirine'), + create_region(world, player, locations_per_region, location_cache, 'Lower Lake Sirine'), + create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'), + create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'), + create_region(world, player, locations_per_region, location_cache, 'Caste Ramparts'), + create_region(world, player, locations_per_region, location_cache, 'Caste Keep'), + create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'), + create_region(world, player, locations_per_region, location_cache, 'Royal towers'), + create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'), + create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'), + create_region(world, player, locations_per_region, location_cache, 'Space time continuum') ] connectStartingRegion(world, player) @@ -149,7 +149,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: pyramid_keys_unlock == "GateMaw") connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment") -def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable) -> Location: +def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable, location_cache: List[Location]) -> Location: location = Location(player, name, id, region) location.access_rule = rule @@ -157,19 +157,23 @@ def create_location(player: int, name: str, id: Optional[int], region: Region, r location.event = True location.locked = True + location_cache.append(location) + return location -def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region: + +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region: region = Region(name, RegionType.Generic, name, player) region.world = world if name in locations_per_region: for location_data in locations_per_region[name]: - location = create_location(player, location_data.name, location_data.code, region, location_data.rule) + location = create_location(player, location_data.name, location_data.code, region, location_data.rule, location_cache) region.locations.append(location) return region + def connectStartingRegion(world: MultiWorld, player: int): menu = world.get_region('Menu', player) tutorial = world.get_region('Tutorial', player) @@ -192,6 +196,7 @@ def connectStartingRegion(world: MultiWorld, player: int): teleport_back_to_start.connect(starting_region) space_time_continuum.exits.append(teleport_back_to_start) + def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source: str, target: str, rule: Optional[Callable] = None): sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) @@ -211,10 +216,11 @@ def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source: sourceRegion.exits.append(connection) connection.connect(targetRegion) -def get_locations_per_region(locations: Tuple[LocationData]) -> Dict[str, List[LocationData]]: + +def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: per_region: Dict[str, List[LocationData]] = {} for location in locations: - per_region[location.region] = [ location ] if location.region not in per_region else per_region[location.region] + [ location ] + per_region.setdefault(location.region, []).append(location) return per_region \ No newline at end of file diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 860c7e84..2adb3ca0 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,52 +1,60 @@ from typing import Dict, List, Set -from BaseClasses import Item, MultiWorld +from BaseClasses import Item, MultiWorld, Location from ..AutoWorld import World from .LogicMixin import TimespinnerLogic -from .Items import item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items +from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items from .Locations import get_locations, starter_progression_locations, EventId from .Regions import create_regions from .Options import is_option_enabled, timespinner_options from .PyramidKeys import get_pyramid_keys_unlock - class TimespinnerWorld(World): + """ + Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. + Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. + """ + options = timespinner_options game = "Timespinner" topology_present = True - data_version = 1 - hidden = True + remote_items = False + data_version = 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_locations(None, None)} + item_name_groups = get_item_names_per_category() locked_locations: Dict[int, List[str]] = {} pyramid_keys_unlock: Dict[int, str] = {} + location_cache: Dict[int, List[Location]] = {} def generate_early(self): self.locked_locations[self.player] = [] + self.location_cache[self.player] = [] self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player) - self.item_name_groups = get_item_name_groups() def create_regions(self): - create_regions(self.world, self.player, get_locations(self.world, self.player), - self.pyramid_keys_unlock[self.player]) + create_regions(self.world, self.player, get_locations(self.world, self.player), + self.location_cache[self.player], self.pyramid_keys_unlock[self.player]) + def create_item(self, name: str) -> Item: return create_item(name, self.player) + def set_rules(self): setup_events(self.world, self.player, self.locked_locations[self.player]) self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player) + def generate_basic(self): excluded_items = get_excluded_items_based_on_options(self.world, self.player) assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player]) - if not is_option_enabled(self.world, self.player, "QuickSeed") or \ - not is_option_enabled(self.world, self.player, "Inverted"): + if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"): place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player]) pool = get_item_pool(self.world, self.player, excluded_items) @@ -55,17 +63,18 @@ class TimespinnerWorld(World): self.world.itempool += pool + def fill_slot_data(self) -> Dict: slot_data = {} for option_name in timespinner_options: - option = getattr(self.world, option_name)[self.player] - slot_data[option_name] = int(option.value) + slot_data[option_name] = is_option_enabled(self.world, self.player, option_name) - slot_data["StinkyMaw"] = 1 - slot_data["ProgressiveVerticalMovement"] = 0 - slot_data["ProgressiveKeycards"] = 0 + slot_data["StinkyMaw"] = True + slot_data["ProgressiveVerticalMovement"] = False + slot_data["ProgressiveKeycards"] = False slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player] + slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player]) return slot_data @@ -106,7 +115,7 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]: - pool = [] + pool: List[Item] = [] for name, data in item_table.items(): if not name in excluded_items: @@ -159,10 +168,11 @@ def setup_events(world: MultiWorld, player: int, locked_locations: List[str]): location.place_locked_item(item) -def get_item_name_groups() -> Dict[str, Set[str]]: - groups: Dict[str, Set[str]] = {} +def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]: + personal_items: Dict[int, int] = {} - for name, data in item_table.items(): - groups.setdefault(data.category, set()).add(name) - - return groups + for location in locations: + if location.address and location.item and location.item.code and location.item.player == player: + personal_items[location.address] = location.item.code + + return personal_items \ No newline at end of file