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

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.
# 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

View File

@ -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),
'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',
@ -255,3 +255,11 @@ filler_items: Tuple[str] = (
'Antidote',
'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
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)),
@ -219,6 +218,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]):
else:
return location_table
starter_progression_locations: Tuple[str, ...] = (
'Starter chest 2',
'Starter chest 3',

View File

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

View File

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

View File

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

View File

@ -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])
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)
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 groups
return personal_items