Fixed some bugs + added documentation + added a few features (#87)

* Refactorings + minor logic fix

* Fixed unnececerly recalculation of item_name_groups

* Enabled other itemId's so that they can be send to client when desired

* Marked the loss of location 1337158

* Updated network graph

* First draft tinmespinner documentation

* Moved personal items to slot_data rather than location scouts

* Disabled Remote Items

* Updated docs

* Fixed port override
This commit is contained in:
Jarno Westhof 2021-09-30 19:51:07 +02:00 committed by GitHub
parent 858d4c74ce
commit cff5db446d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 264 additions and 132 deletions

View File

@ -10,6 +10,7 @@ Currently, the following games are supported:
* Slay the Spire * Slay the Spire
* Risk of Rain 2 * Risk of Rain 2
* The Legend of Zelda: Ocarina of Time * The Legend of Zelda: Ocarina of Time
* Timespinner
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial). 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 Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -0,0 +1,28 @@
# Timespinner
## Where is the settings page?
The player settings page for this game is located <a href="../player-settings">here</a>. 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

View File

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

View File

@ -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"
]
}
]
}
]
} }
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -81,7 +81,7 @@ class World(metaclass=AutoWorldRegister):
# increment this every time something in your world's names/id mappings changes. # 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 # 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. # 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 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 forced_auto_forfeit: bool = False
# Hide World Type from various views. Does not remove functionality. # Hide World Type from various views. Does not remove functionality.
hidden = False hidden: bool = False
# autoset on creation: # autoset on creation:
world: MultiWorld world: MultiWorld

View File

@ -1,4 +1,4 @@
from typing import Dict, Tuple, NamedTuple from typing import Dict, Set, Tuple, NamedTuple
class ItemData(NamedTuple): class ItemData(NamedTuple):
category: str 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 # 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] = { item_table: Dict[str, ItemData] = {
'Eternal Crown': ItemData('Equipment', 1337000), 'Eternal Crown': ItemData('Equipment', 1337000),
#'Security Visor': ItemData('Equipment', 1337001), 'Security Visor': ItemData('Equipment', 1337001, 0),
#'Engineer Goggles': ItemData('Equipment', 1337002), 'Engineer Goggles': ItemData('Equipment', 1337002, 0),
#'Leather Helmet': ItemData('Equipment', 1337003), 'Leather Helmet': ItemData('Equipment', 1337003, 0),
#'Copper Helmet': ItemData('Equipment', 1337004), 'Copper Helmet': ItemData('Equipment', 1337004, 0),
'Pointy Hat': ItemData('Equipment', 1337005), 'Pointy Hat': ItemData('Equipment', 1337005),
#'Dragoon Helmet': ItemData('Equipment', 1337006), 'Dragoon Helmet': ItemData('Equipment', 1337006, 0),
'Buckle Hat': ItemData('Equipment', 1337007), 'Buckle Hat': ItemData('Equipment', 1337007),
#'Advisor Hat': ItemData('Equipment', 1337008), 'Advisor Hat': ItemData('Equipment', 1337008, 0),
'Librarian Hat': ItemData('Equipment', 1337009), 'Librarian Hat': ItemData('Equipment', 1337009),
#'Combat Helmet': ItemData('Equipment', 1337010), 'Combat Helmet': ItemData('Equipment', 1337010, 0),
'Captain\'s Cap': ItemData('Equipment', 1337011), 'Captain\'s Cap': ItemData('Equipment', 1337011),
'Lab Glasses': ItemData('Equipment', 1337012), 'Lab Glasses': ItemData('Equipment', 1337012),
'Empire Crown': ItemData('Equipment', 1337013), 'Empire Crown': ItemData('Equipment', 1337013),
'Viletian Crown': ItemData('Equipment', 1337014), 'Viletian Crown': ItemData('Equipment', 1337014),
#'Sunglasses': ItemData('Equipment', 1337015), 'Sunglasses': ItemData('Equipment', 1337015, 0),
'Old Coat': ItemData('Equipment', 1337016), 'Old Coat': ItemData('Equipment', 1337016),
#'Trendy Jacket': ItemData('Equipment', 1337017), 'Trendy Jacket': ItemData('Equipment', 1337017, 0),
#'Security Vest': ItemData('Equipment', 1337018), 'Security Vest': ItemData('Equipment', 1337018, 0),
#'Leather Jerkin': ItemData('Equipment', 1337019), 'Leather Jerkin': ItemData('Equipment', 1337019, 0),
#'Copper Breastplate': ItemData('Equipment', 1337020), 'Copper Breastplate': ItemData('Equipment', 1337020, 0),
'Traveler\'s Cloak': ItemData('Equipment', 1337021), 'Traveler\'s Cloak': ItemData('Equipment', 1337021),
#'Dragoon Armor': ItemData('Equipment', 1337022), 'Dragoon Armor': ItemData('Equipment', 1337022, 0),
'Midnight Cloak': ItemData('Equipment', 1337023), 'Midnight Cloak': ItemData('Equipment', 1337023),
#'Advisor Robe': ItemData('Equipment', 1337024), 'Advisor Robe': ItemData('Equipment', 1337024, 0),
'Librarian Robe': ItemData('Equipment', 1337025), 'Librarian Robe': ItemData('Equipment', 1337025),
#'Military Armor': ItemData('Equipment', 1337026), 'Military Armor': ItemData('Equipment', 1337026, 0),
'Captain\'s Uniform': ItemData('Equipment', 1337027), 'Captain\'s Uniform': ItemData('Equipment', 1337027),
'Lab Coat': ItemData('Equipment', 1337028), 'Lab Coat': ItemData('Equipment', 1337028),
'Empress Robe': ItemData('Equipment', 1337029), 'Empress Robe': ItemData('Equipment', 1337029),
'Princess Dress': ItemData('Equipment', 1337030), 'Princess Dress': ItemData('Equipment', 1337030),
'Eternal Coat': ItemData('Equipment', 1337031), 'Eternal Coat': ItemData('Equipment', 1337031),
#'Synthetic Plume': ItemData('Equipment', 1337032), 'Synthetic Plume': ItemData('Equipment', 1337032, 0),
#'Cheveur Plume': ItemData('Equipment', 1337033), 'Cheveur Plume': ItemData('Equipment', 1337033, 0),
'Metal Wristband': ItemData('Equipment', 1337034), 'Metal Wristband': ItemData('Equipment', 1337034),
#'Nymph Hairband': ItemData('Equipment', 1337035), 'Nymph Hairband': ItemData('Equipment', 1337035, 0),
#'Mother o\' Pearl': ItemData('Equipment', 1337036), 'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
'Bird Statue': ItemData('Equipment', 1337037), 'Bird Statue': ItemData('Equipment', 1337037),
#'Chaos Stole': ItemData('Equipment', 1337038), 'Chaos Stole': ItemData('Equipment', 1337038, 0),
'Pendulum': ItemData('Equipment', 1337039), 'Pendulum': ItemData('Equipment', 1337039),
#'Chaos Horn': ItemData('Equipment', 1337040), 'Chaos Horn': ItemData('Equipment', 1337040, 0),
'Filigree Clasp': ItemData('Equipment', 1337041), 'Filigree Clasp': ItemData('Equipment', 1337041),
#'Azure Stole': ItemData('Equipment', 1337042), 'Azure Stole': ItemData('Equipment', 1337042, 0),
'Ancient Coin': ItemData('Equipment', 1337043), 'Ancient Coin': ItemData('Equipment', 1337043),
#'Shiny Rock': ItemData('Equipment', 1337044), 'Shiny Rock': ItemData('Equipment', 1337044, 0),
'Galaxy Earrings': ItemData('Equipment', 1337045), 'Galaxy Earrings': ItemData('Equipment', 1337045),
'Selen\'s Bangle': ItemData('Equipment', 1337046), 'Selen\'s Bangle': ItemData('Equipment', 1337046),
'Glass Pumpkin': ItemData('Equipment', 1337047), 'Glass Pumpkin': ItemData('Equipment', 1337047),
@ -76,45 +76,45 @@ item_table: Dict[str, ItemData] = {
'Antidote': ItemData('UseItem', 1337065, 0), 'Antidote': ItemData('UseItem', 1337065, 0),
'Chaos Rose': ItemData('UseItem', 1337066, 0), 'Chaos Rose': ItemData('UseItem', 1337066, 0),
'Warp Shard': ItemData('UseItem', 1337067), 'Warp Shard': ItemData('UseItem', 1337067),
#'Dream Wisp': ItemData('UseItem', 1337068), 'Dream Wisp': ItemData('UseItem', 1337068, 0),
#'PlaceHolderItem1': ItemData('UseItem', 1337069), 'PlaceHolderItem1': ItemData('UseItem', 1337069, 0),
#'Lachiemi Sun': ItemData('UseItem', 1337070), 'Lachiemi Sun': ItemData('UseItem', 1337070, 0),
'Jerky': ItemData('UseItem', 1337071), 'Jerky': ItemData('UseItem', 1337071),
#'Biscuit': ItemData('UseItem', 1337072), 'Biscuit': ItemData('UseItem', 1337072, 0),
#'Fried Cheveur': ItemData('UseItem', 1337073), 'Fried Cheveur': ItemData('UseItem', 1337073, 0),
#'Sautéed Wyvern Tail': ItemData('UseItem', 1337074), 'Sautéed Wyvern Tail': ItemData('UseItem', 1337074, 0),
#'Unagi Roll': ItemData('UseItem', 1337075), 'Unagi Roll': ItemData('UseItem', 1337075, 0),
#'Cheveur au Vin': ItemData('UseItem', 1337076), 'Cheveur au Vin': ItemData('UseItem', 1337076, 0),
#'Royal Casserole': ItemData('UseItem', 1337077), 'Royal Casserole': ItemData('UseItem', 1337077, 0),
'Spaghetti': ItemData('UseItem', 1337078), 'Spaghetti': ItemData('UseItem', 1337078),
#'Plump Maggot': ItemData('UseItem', 1337079), 'Plump Maggot': ItemData('UseItem', 1337079, 0),
#'Orange Juice': ItemData('UseItem', 1337080), 'Orange Juice': ItemData('UseItem', 1337080, 0),
'Filigree Tea': ItemData('UseItem', 1337081), 'Filigree Tea': ItemData('UseItem', 1337081),
#'Empress Cake': ItemData('UseItem', 1337082), 'Empress Cake': ItemData('UseItem', 1337082, 0),
#'Rotten Tail': ItemData('UseItem', 1337083), 'Rotten Tail': ItemData('UseItem', 1337083, 0),
#'Alchemy Tools': ItemData('UseItem', 1337084), 'Alchemy Tools': ItemData('UseItem', 1337084, 0),
'Galaxy Stone': ItemData('UseItem', 1337085), 'Galaxy Stone': ItemData('UseItem', 1337085),
#1337086 Used interally # 1337086 Used interally
#'Essence Crystal': ItemData('UseItem', 1337087), 'Essence Crystal': ItemData('UseItem', 1337087, 0),
#'Gold Ring': ItemData('UseItem', 1337088), 'Gold Ring': ItemData('UseItem', 1337088, 0),
#'Gold Necklace': ItemData('UseItem', 1337089), 'Gold Necklace': ItemData('UseItem', 1337089, 0),
'Herb': ItemData('UseItem', 1337090), 'Herb': ItemData('UseItem', 1337090),
#'Mushroom': ItemData('UseItem', 1337091), 'Mushroom': ItemData('UseItem', 1337091, 0),
#'Plasma Crystal': ItemData('UseItem', 1337092), 'Plasma Crystal': ItemData('UseItem', 1337092, 0),
'Plasma IV Bag': ItemData('UseItem', 1337093), 'Plasma IV Bag': ItemData('UseItem', 1337093),
#'Cheveur Drumstick': ItemData('UseItem', 1337094), 'Cheveur Drumstick': ItemData('UseItem', 1337094, 0),
#'Wyvern Tail': ItemData('UseItem', 1337095), 'Wyvern Tail': ItemData('UseItem', 1337095, 0),
#'Eel Meat': ItemData('UseItem', 1337096), 'Eel Meat': ItemData('UseItem', 1337096, 0),
#'Cheveux Breast': ItemData('UseItem', 1337097), 'Cheveux Breast': ItemData('UseItem', 1337097, 0),
'Food Synthesizer': ItemData('UseItem', 1337098), 'Food Synthesizer': ItemData('UseItem', 1337098),
#'Cheveux Feather': ItemData('UseItem', 1337099), 'Cheveux Feather': ItemData('UseItem', 1337099, 0),
#'Siren Ink': ItemData('UseItem', 1337100), 'Siren Ink': ItemData('UseItem', 1337100, 0),
#'Plasma Core': ItemData('UseItem', 1337101), 'Plasma Core': ItemData('UseItem', 1337101, 0),
#'Silver Ore': ItemData('UseItem', 1337102), 'Silver Ore': ItemData('UseItem', 1337102, 0),
#'Historical Documents': ItemData('UseItem', 1337103), 'Historical Documents': ItemData('UseItem', 1337103, 0),
#'MapReveal 0': ItemData('UseItem', 1337104), 'MapReveal 0': ItemData('UseItem', 1337104, 0),
#'MapReveal 1': ItemData('UseItem', 1337105), 'MapReveal 1': ItemData('UseItem', 1337105, 0),
#'MapReveal 2': ItemData('UseItem', 1337106), 'MapReveal 2': ItemData('UseItem', 1337106, 0),
'Timespinner Wheel': ItemData('Relic', 1337107, progression=True), 'Timespinner Wheel': ItemData('Relic', 1337107, progression=True),
'Timespinner Spindle': ItemData('Relic', 1337108, progression=True), 'Timespinner Spindle': ItemData('Relic', 1337108, progression=True),
'Timespinner Gear 1': ItemData('Relic', 1337109, 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) 'Max Sand': ItemData('Stat', 1337249, 14)
} }
starter_melee_weapons: Tuple[str] = ( starter_melee_weapons: Tuple[str, ...] = (
'Blue Orb', 'Blue Orb',
'Blade Orb', 'Blade Orb',
'Fire Orb', 'Fire Orb',
@ -211,7 +211,7 @@ starter_melee_weapons: Tuple[str] = (
'Radiant Orb' 'Radiant Orb'
) )
starter_spells: Tuple[str] = ( starter_spells: Tuple[str, ...] = (
'Colossal Blade', 'Colossal Blade',
'Infernal Flames', 'Infernal Flames',
'Plasma Geyser', 'Plasma Geyser',
@ -229,7 +229,7 @@ starter_spells: Tuple[str] = (
) )
# weighted # weighted
starter_progression_items: Tuple[str] = ( starter_progression_items: Tuple[str, ...] = (
'Talaria Attachment', 'Talaria Attachment',
'Talaria Attachment', 'Talaria Attachment',
'Succubus Hairpin', 'Succubus Hairpin',
@ -241,7 +241,7 @@ starter_progression_items: Tuple[str] = (
'Lightwall' 'Lightwall'
) )
filler_items: Tuple[str] = ( filler_items: Tuple[str, ...] = (
'Potion', 'Potion',
'Ether', 'Ether',
'Hi-Potion', 'Hi-Potion',
@ -254,4 +254,12 @@ filler_items: Tuple[str] = (
'Mind Refresh ULTRA', 'Mind Refresh ULTRA',
'Antidote', 'Antidote',
'Chaos Rose' 'Chaos Rose'
) )
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

View File

@ -4,14 +4,12 @@ from .Options import is_option_enabled
EventId: Optional[int] = None EventId: Optional[int] = None
class LocationData(NamedTuple): class LocationData(NamedTuple):
region: str region: str
name: str name: str
code: Optional[int] code: Optional[int]
rule: Callable = lambda state: True rule: Callable = lambda state: True
def get_locations(world: Optional[MultiWorld], player: Optional[int]): def get_locations(world: Optional[MultiWorld], player: Optional[int]):
location_table: Tuple[LocationData, ...] = ( location_table: Tuple[LocationData, ...] = (
# PresentItemLocations # PresentItemLocations
@ -200,6 +198,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]):
# DownloadTerminals # DownloadTerminals
LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)), LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
LocationData('Libary', 'Library terminal 2', 1337156, 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', '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 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)), 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 ) return ( *location_table, *downloadable_items )
else: else:
return location_table return location_table
starter_progression_locations: Tuple[str, ...] = ( starter_progression_locations: Tuple[str, ...] = (
'Starter chest 2', 'Starter chest 2',

View File

@ -19,7 +19,7 @@ class DownloadableItems(Toggle):
display_name = "Downloadable items" display_name = "Downloadable items"
class FacebookMode(Toggle): 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" display_name = "Facebook mode"
class StartWithMeyef(Toggle): class StartWithMeyef(Toggle):

View File

@ -3,7 +3,7 @@ from BaseClasses import MultiWorld
from .Options import is_option_enabled from .Options import is_option_enabled
def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str: def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
present_teleportation_gates: Tuple[str] = ( present_teleportation_gates: Tuple[str, ...] = (
"GateKittyBoss", "GateKittyBoss",
"GateLeftLibrary", "GateLeftLibrary",
"GateMilitairyGate", "GateMilitairyGate",
@ -12,7 +12,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
"GateLakeDesolation" "GateLakeDesolation"
) )
past_teleportation_gates: Tuple[str] = ( past_teleportation_gates: Tuple[str, ...] = (
"GateLakeSirineRight", "GateLakeSirineRight",
"GateAccessToPast", "GateAccessToPast",
"GateCastleRamparts", "GateCastleRamparts",

View File

@ -3,46 +3,46 @@ from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
from .Options import is_option_enabled from .Options import is_option_enabled
from .Locations import LocationData 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) locations_per_region = get_locations_per_region(locations)
world.regions += [ world.regions += [
create_region(world, player, locations_per_region, 'Menu'), create_region(world, player, locations_per_region, location_cache, 'Menu'),
create_region(world, player, locations_per_region, 'Tutorial'), create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
create_region(world, player, locations_per_region, 'Lake desolation'), create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
create_region(world, player, locations_per_region, 'Upper lake desolation'), create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'),
create_region(world, player, locations_per_region, 'Lower lake desolation'), create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'),
create_region(world, player, locations_per_region, 'Libary'), create_region(world, player, locations_per_region, location_cache, 'Libary'),
create_region(world, player, locations_per_region, 'Libary top'), create_region(world, player, locations_per_region, location_cache, 'Libary top'),
create_region(world, player, locations_per_region, 'Varndagroth tower left'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'),
create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'),
create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'),
create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'),
create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'), create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'),
create_region(world, player, locations_per_region, 'Militairy Fortress'), create_region(world, player, locations_per_region, location_cache, 'Militairy Fortress'),
create_region(world, player, locations_per_region, 'The lab'), create_region(world, player, locations_per_region, location_cache, 'The lab'),
create_region(world, player, locations_per_region, 'The lab (power off)'), create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'),
create_region(world, player, locations_per_region, 'The lab (upper)'), create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'),
create_region(world, player, locations_per_region, 'Emperors tower'), create_region(world, player, locations_per_region, location_cache, 'Emperors tower'),
create_region(world, player, locations_per_region, 'Skeleton Shaft'), create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'),
create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'),
create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'),
create_region(world, player, locations_per_region, 'Refugee Camp'), create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'),
create_region(world, player, locations_per_region, 'Forest'), create_region(world, player, locations_per_region, location_cache, 'Forest'),
create_region(world, player, locations_per_region, 'Left Side forest Caves'), create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'),
create_region(world, player, locations_per_region, 'Upper Lake Sirine'), create_region(world, player, locations_per_region, location_cache, 'Upper Lake Sirine'),
create_region(world, player, locations_per_region, 'Lower Lake Sirine'), create_region(world, player, locations_per_region, location_cache, 'Lower Lake Sirine'),
create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'), create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'),
create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'), create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'),
create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'), create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'),
create_region(world, player, locations_per_region, 'Caste Ramparts'), create_region(world, player, locations_per_region, location_cache, 'Caste Ramparts'),
create_region(world, player, locations_per_region, 'Caste Keep'), create_region(world, player, locations_per_region, location_cache, 'Caste Keep'),
create_region(world, player, locations_per_region, 'Royal towers (lower)'), create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'),
create_region(world, player, locations_per_region, 'Royal towers'), create_region(world, player, locations_per_region, location_cache, 'Royal towers'),
create_region(world, player, locations_per_region, 'Royal towers (upper)'), create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'),
create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'), create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'),
create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'), create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'),
create_region(world, player, locations_per_region, 'Space time continuum') create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
] ]
connectStartingRegion(world, player) 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 (Maw)', lambda state: pyramid_keys_unlock == "GateMaw")
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment") 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 = Location(player, name, id, region)
location.access_rule = rule 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.event = True
location.locked = True location.locked = True
location_cache.append(location)
return 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 = Region(name, RegionType.Generic, name, player)
region.world = world region.world = world
if name in locations_per_region: if name in locations_per_region:
for location_data in locations_per_region[name]: 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) region.locations.append(location)
return region return region
def connectStartingRegion(world: MultiWorld, player: int): def connectStartingRegion(world: MultiWorld, player: int):
menu = world.get_region('Menu', player) menu = world.get_region('Menu', player)
tutorial = world.get_region('Tutorial', player) tutorial = world.get_region('Tutorial', player)
@ -192,6 +196,7 @@ def connectStartingRegion(world: MultiWorld, player: int):
teleport_back_to_start.connect(starting_region) teleport_back_to_start.connect(starting_region)
space_time_continuum.exits.append(teleport_back_to_start) 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): 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) sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, 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) sourceRegion.exits.append(connection)
connection.connect(targetRegion) 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]] = {} per_region: Dict[str, List[LocationData]] = {}
for location in locations: 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 return per_region

View File

@ -1,52 +1,60 @@
from typing import Dict, List, Set from typing import Dict, List, Set
from BaseClasses import Item, MultiWorld from BaseClasses import Item, MultiWorld, Location
from ..AutoWorld import World from ..AutoWorld import World
from .LogicMixin import TimespinnerLogic 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 .Locations import get_locations, starter_progression_locations, EventId
from .Regions import create_regions from .Regions import create_regions
from .Options import is_option_enabled, timespinner_options from .Options import is_option_enabled, timespinner_options
from .PyramidKeys import get_pyramid_keys_unlock from .PyramidKeys import get_pyramid_keys_unlock
class TimespinnerWorld(World): 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 options = timespinner_options
game = "Timespinner" game = "Timespinner"
topology_present = True topology_present = True
data_version = 1 remote_items = False
hidden = True data_version = 2
item_name_to_id = {name: data.code for name, data in item_table.items()} item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)} 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]] = {} locked_locations: Dict[int, List[str]] = {}
pyramid_keys_unlock: Dict[int, str] = {} pyramid_keys_unlock: Dict[int, str] = {}
location_cache: Dict[int, List[Location]] = {}
def generate_early(self): def generate_early(self):
self.locked_locations[self.player] = [] 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.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): def create_regions(self):
create_regions(self.world, self.player, get_locations(self.world, self.player), create_regions(self.world, self.player, get_locations(self.world, self.player),
self.pyramid_keys_unlock[self.player]) self.location_cache[self.player], self.pyramid_keys_unlock[self.player])
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
return create_item(name, self.player) return create_item(name, self.player)
def set_rules(self): def set_rules(self):
setup_events(self.world, self.player, self.locked_locations[self.player]) 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) self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
def generate_basic(self): def generate_basic(self):
excluded_items = get_excluded_items_based_on_options(self.world, self.player) 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]) 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 \ if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
not is_option_enabled(self.world, self.player, "Inverted"):
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player]) 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) pool = get_item_pool(self.world, self.player, excluded_items)
@ -55,17 +63,18 @@ class TimespinnerWorld(World):
self.world.itempool += pool self.world.itempool += pool
def fill_slot_data(self) -> Dict: def fill_slot_data(self) -> Dict:
slot_data = {} slot_data = {}
for option_name in timespinner_options: for option_name in timespinner_options:
option = getattr(self.world, option_name)[self.player] slot_data[option_name] = is_option_enabled(self.world, self.player, option_name)
slot_data[option_name] = int(option.value)
slot_data["StinkyMaw"] = 1 slot_data["StinkyMaw"] = True
slot_data["ProgressiveVerticalMovement"] = 0 slot_data["ProgressiveVerticalMovement"] = False
slot_data["ProgressiveKeycards"] = 0 slot_data["ProgressiveKeycards"] = False
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player] 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 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]: 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(): for name, data in item_table.items():
if not name in excluded_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) location.place_locked_item(item)
def get_item_name_groups() -> Dict[str, Set[str]]: def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]:
groups: Dict[str, Set[str]] = {} personal_items: Dict[int, int] = {}
for name, data in item_table.items(): for location in locations:
groups.setdefault(data.category, set()).add(name) if location.address and location.item and location.item.code and location.item.player == player:
personal_items[location.address] = location.item.code
return groups
return personal_items