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:
parent
858d4c74ce
commit
cff5db446d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
BIN
docs/network.png
BIN
docs/network.png
Binary file not shown.
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 109 KiB |
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
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
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue