diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py new file mode 100644 index 00000000..4f45b658 --- /dev/null +++ b/worlds/timespinner/Items.py @@ -0,0 +1,257 @@ +from typing import Dict, Tuple, NamedTuple + +class ItemData(NamedTuple): + category: str + code: int + count: int = 1 + progression: bool = False + +# 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), + 'Pointy Hat': ItemData('Equipment', 1337005), + #'Dragoon Helmet': ItemData('Equipment', 1337006), + 'Buckle Hat': ItemData('Equipment', 1337007), + #'Advisor Hat': ItemData('Equipment', 1337008), + 'Librarian Hat': ItemData('Equipment', 1337009), + #'Combat Helmet': ItemData('Equipment', 1337010), + '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), + '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), + 'Traveler\'s Cloak': ItemData('Equipment', 1337021), + #'Dragoon Armor': ItemData('Equipment', 1337022), + 'Midnight Cloak': ItemData('Equipment', 1337023), + #'Advisor Robe': ItemData('Equipment', 1337024), + 'Librarian Robe': ItemData('Equipment', 1337025), + #'Military Armor': ItemData('Equipment', 1337026), + '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), + 'Metal Wristband': ItemData('Equipment', 1337034), + #'Nymph Hairband': ItemData('Equipment', 1337035), + #'Mother o\' Pearl': ItemData('Equipment', 1337036), + 'Bird Statue': ItemData('Equipment', 1337037), + #'Chaos Stole': ItemData('Equipment', 1337038), + 'Pendulum': ItemData('Equipment', 1337039), + #'Chaos Horn': ItemData('Equipment', 1337040), + 'Filigree Clasp': ItemData('Equipment', 1337041), + #'Azure Stole': ItemData('Equipment', 1337042), + 'Ancient Coin': ItemData('Equipment', 1337043), + #'Shiny Rock': ItemData('Equipment', 1337044), + 'Galaxy Earrings': ItemData('Equipment', 1337045), + 'Selen\'s Bangle': ItemData('Equipment', 1337046), + 'Glass Pumpkin': ItemData('Equipment', 1337047), + 'Gilded Egg': ItemData('Equipment', 1337048), + 'Meyef': ItemData('Familiar', 1337049), + 'Griffin': ItemData('Familiar', 1337050), + 'Merchant Crow': ItemData('Familiar', 1337051), + 'Kobo': ItemData('Familiar', 1337052), + 'Sprite': ItemData('Familiar', 1337053), + 'Demon': ItemData('Familiar', 1337054), + 'Potion': ItemData('UseItem', 1337055, 0), + 'Ether': ItemData('UseItem', 1337056, 0), + 'Sand Vial': ItemData('UseItem', 1337057, 0), + 'Hi-Potion': ItemData('UseItem', 1337058, 0), + 'Hi-Ether': ItemData('UseItem', 1337059, 0), + 'Sand Bottle': ItemData('UseItem', 1337060, 0), + 'Berry Pick-Mi-Up': ItemData('UseItem', 1337061, 0), + 'Berry Pick-Mi-Up+': ItemData('UseItem', 1337062, 0), + 'Mind Refresh': ItemData('UseItem', 1337063, 0), + 'Mind Refresh ULTRA': ItemData('UseItem', 1337064, 0), + '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), + '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), + 'Spaghetti': ItemData('UseItem', 1337078), + #'Plump Maggot': ItemData('UseItem', 1337079), + #'Orange Juice': ItemData('UseItem', 1337080), + 'Filigree Tea': ItemData('UseItem', 1337081), + #'Empress Cake': ItemData('UseItem', 1337082), + #'Rotten Tail': ItemData('UseItem', 1337083), + #'Alchemy Tools': ItemData('UseItem', 1337084), + 'Galaxy Stone': ItemData('UseItem', 1337085), + #1337086 Used interally + #'Essence Crystal': ItemData('UseItem', 1337087), + #'Gold Ring': ItemData('UseItem', 1337088), + #'Gold Necklace': ItemData('UseItem', 1337089), + 'Herb': ItemData('UseItem', 1337090), + #'Mushroom': ItemData('UseItem', 1337091), + #'Plasma Crystal': ItemData('UseItem', 1337092), + '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), + '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), + 'Timespinner Wheel': ItemData('Relic', 1337107, progression=True), + 'Timespinner Spindle': ItemData('Relic', 1337108, progression=True), + 'Timespinner Gear 1': ItemData('Relic', 1337109, progression=True), + 'Timespinner Gear 2': ItemData('Relic', 1337110, progression=True), + 'Timespinner Gear 3': ItemData('Relic', 1337111, progression=True), + 'Twin Pyramid Key': ItemData('Relic', 1337112, progression=True), + 'Celestial Sash': ItemData('Relic', 1337113, progression=True), + 'Succubus Hairpin': ItemData('Relic', 1337114, progression=True), + 'Talaria Attachment': ItemData('Relic', 1337115, progression=True), + 'Water Mask': ItemData('Relic', 1337116, progression=True), + 'Gas Mask': ItemData('Relic', 1337117, progression=True), + 'Soul Scanner': ItemData('Relic', 1337118), + 'Security Keycard A': ItemData('Relic', 1337119, progression=True), + 'Security Keycard B': ItemData('Relic', 1337120, progression=True), + 'Security Keycard C': ItemData('Relic', 1337121, progression=True), + 'Security Keycard D': ItemData('Relic', 1337122, progression=True), + 'Library Keycard V': ItemData('Relic', 1337123, progression=True), + 'Tablet': ItemData('Relic', 1337124, progression=True), + 'Elevator Keycard': ItemData('Relic', 1337125, progression=True), + 'Jewelry Box': ItemData('Relic', 1337126), + 'Goddess Brooch': ItemData('Relic', 1337127), + 'Wyrm Brooch': ItemData('Relic', 1337128), + 'Greed Brooch': ItemData('Relic', 1337129), + 'Eternal Brooch': ItemData('Relic', 1337130), + 'Blue Orb': ItemData('Orb Melee', 1337131), + 'Blade Orb': ItemData('Orb Melee', 1337132), + 'Fire Orb': ItemData('Orb Melee', 1337133, progression=True), + 'Plasma Orb': ItemData('Orb Melee', 1337134, progression=True), + 'Iron Orb': ItemData('Orb Melee', 1337135), + 'Ice Orb': ItemData('Orb Melee', 1337136), + 'Wind Orb': ItemData('Orb Melee', 1337137), + 'Gun Orb': ItemData('Orb Melee', 1337138), + 'Umbra Orb': ItemData('Orb Melee', 1337139), + 'Empire Orb': ItemData('Orb Melee', 1337140), + 'Eye Orb': ItemData('Orb Melee', 1337141), + 'Blood Orb': ItemData('Orb Melee', 1337142), + 'Forbidden Tome': ItemData('Orb Melee', 1337143), + 'Shattered Orb': ItemData('Orb Melee', 1337144), + 'Nether Orb': ItemData('Orb Melee', 1337145), + 'Radiant Orb': ItemData('Orb Melee', 1337146), + 'Aura Blast': ItemData('Orb Spell', 1337147), + 'Colossal Blade': ItemData('Orb Spell', 1337148), + 'Infernal Flames': ItemData('Orb Spell', 1337149, progression=True), + 'Plasma Geyser': ItemData('Orb Spell', 1337150, progression=True), + 'Colossal Hammer': ItemData('Orb Spell', 1337151), + 'Frozen Spires': ItemData('Orb Spell', 1337152), + 'Storm Eye': ItemData('Orb Spell', 1337153), + 'Arm Cannon': ItemData('Orb Spell', 1337154), + 'Dark Flames': ItemData('Orb Spell', 1337155), + 'Aura Serpent': ItemData('Orb Spell', 1337156), + 'Chaos Blades': ItemData('Orb Spell', 1337157), + 'Crimson Vortex': ItemData('Orb Spell', 1337158), + 'Djinn Inferno': ItemData('Orb Spell', 1337159, progression=True), + 'Bombardment': ItemData('Orb Spell', 1337160), + 'Corruption': ItemData('Orb Spell', 1337161), + 'Lightwall': ItemData('Orb Spell', 1337162, progression=True), + 'Bleak Ring': ItemData('Orb Passive', 1337163), + 'Scythe Ring': ItemData('Orb Passive', 1337164), + 'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True), + 'Royal Ring': ItemData('Orb Passive', 1337166, progression=True), + 'Shield Ring': ItemData('Orb Passive', 1337167), + 'Icicle Ring': ItemData('Orb Passive', 1337168), + 'Tailwind Ring': ItemData('Orb Passive', 1337169), + 'Economizer Ring': ItemData('Orb Passive', 1337170), + 'Dusk Ring': ItemData('Orb Passive', 1337171), + 'Star of Lachiem': ItemData('Orb Passive', 1337172), + 'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True), + 'Sanguine Ring': ItemData('Orb Passive', 1337174), + 'Sun Ring': ItemData('Orb Passive', 1337175), + 'Silence Ring': ItemData('Orb Passive', 1337176), + 'Shadow Seal': ItemData('Orb Passive', 1337177), + 'Hope Ring': ItemData('Orb Passive', 1337178), + 'Max HP': ItemData('Stat', 1337179, 12), + 'Max Aura': ItemData('Stat', 1337180, 13), + # 1337181 - 1337248 Reserved + 'Max Sand': ItemData('Stat', 1337249, 14) +} + +starter_melee_weapons: Tuple[str] = ( + 'Blue Orb', + 'Blade Orb', + 'Fire Orb', + 'Iron Orb', + 'Ice Orb', + 'Wind Orb', + 'Gun Orb', + 'Umbra Orb', + 'Empire Orb', + 'Eye Orb', + 'Blood Orb', + 'Forbidden Tome', + 'Shattered Orb', + 'Nether Orb', + 'Radiant Orb' +) + +starter_spells: Tuple[str] = ( + 'Colossal Blade', + 'Infernal Flames', + 'Plasma Geyser', + 'Colossal Hammer', + 'Frozen Spires', + 'Storm Eye', + 'Arm Cannon', + 'Dark Flames', + 'Aura Serpent', + 'Chaos Blades', + 'Crimson Vortex', + 'Djinn Inferno', + 'Bombardment', + 'Corruption' +) + +# weighted +starter_progression_items: Tuple[str] = ( + 'Talaria Attachment', + 'Talaria Attachment', + 'Succubus Hairpin', + 'Succubus Hairpin', + 'Timespinner Wheel', + 'Timespinner Wheel', + 'Twin Pyramid Key', + 'Celestial Sash', + 'Lightwall' +) + +filler_items: Tuple[str] = ( + 'Potion', + 'Ether', + 'Hi-Potion', + 'Hi-Ether', + 'Sand Vial', + 'Sand Bottle', + 'Berry Pick-Mi-Up', + 'Berry Pick-Mi-Up+', + 'Mind Refresh', + 'Mind Refresh ULTRA', + 'Antidote', + 'Chaos Rose' +) \ No newline at end of file diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py new file mode 100644 index 00000000..ac9da501 --- /dev/null +++ b/worlds/timespinner/Locations.py @@ -0,0 +1,225 @@ +from typing import Tuple, Optional, Callable, NamedTuple +from BaseClasses import MultiWorld +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: MultiWorld, player: int): + location_table: Tuple[LocationData] = ( + # PresentItemLocations + LocationData('Tutorial', 'Yo Momma 1', 1337000), + LocationData('Tutorial', 'Yo Momma 2', 1337001), + LocationData('Lake desolation', 'Starter chest 2', 1337002), + LocationData('Lake desolation', 'Starter chest 3', 1337003), + LocationData('Lake desolation', 'Starter chest 1', 1337004), + LocationData('Lake desolation', 'Timespinner Wheel room', 1337005), + LocationData('Upper lake desolation', 'Forget me not chest', 1337006), + LocationData('Lower lake desolation', 'Chicken chest', 1337007, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Lower lake desolation', 'Not so secret room', 1337008, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Lower lake desolation', 'Tank chest', 1337009, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Upper lake desolation', 'Oxygen recovery room', 1337010), + LocationData('Upper lake desolation', 'Lake secret', 1337011, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Upper lake desolation', 'Double jump cave floor', 1337012, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Upper lake desolation', 'Double jump cave platform', 1337013), + LocationData('Upper lake desolation', 'Fire-Locked sparrow chest', 1337014), + LocationData('Upper lake desolation', 'Crash site pedestal', 1337015), + LocationData('Upper lake desolation', 'Crash site chest 1', 1337016, lambda state: state.has_all(['Killed Maw', 'Gas Mask'], player)), + LocationData('Upper lake desolation', 'Crash site chest 2', 1337017, lambda state: state.has_all(['Killed Maw', 'Gas Mask'], player)), + LocationData('Upper lake desolation', 'Kitty Boss', 1337018), + LocationData('Libary', 'Basement', 1337019), + LocationData('Libary', 'Consolation', 1337020), + LocationData('Libary', 'Librarian', 1337021), + LocationData('Libary', 'Reading nook chest', 1337022), + LocationData('Libary', 'Storage room chest 1', 1337023, lambda state: state._timespinner_has_keycard_D(world, player)), + LocationData('Libary', 'Storage room chest 2', 1337024, lambda state: state._timespinner_has_keycard_D(world, player)), + LocationData('Libary', 'Storage room chest 3', 1337025, lambda state: state._timespinner_has_keycard_D(world, player)), + LocationData('Libary top', 'Backer room chest 5', 1337026), + LocationData('Libary top', 'Backer room chest 4', 1337027), + LocationData('Libary top', 'Backer room chest 3', 1337028), + LocationData('Libary top', 'Backer room chest 2', 1337029), + LocationData('Libary top', 'Backer room chest 1', 1337030), + LocationData('Varndagroth tower left', 'Elevator Key not required', 1337031), + LocationData('Varndagroth tower left', 'Ye olde Timespinner', 1337032), + LocationData('Varndagroth tower left', 'C Keycard chest', 1337033, lambda state: state._timespinner_has_keycard_C(world, player)), + LocationData('Varndagroth tower left', 'Left air vents secret', 1337034, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Varndagroth tower left', 'Left elevator chest', 1337035, lambda state: state.has('Elevator Keycard', player)), + LocationData('Varndagroth tower right (upper)', 'Spider heck room', 1337036), + LocationData('Varndagroth tower right (elevator)', 'Right elevator chest', 1337037), + LocationData('Varndagroth tower right (upper)', 'Elevator card chest', 1337038, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), + LocationData('Varndagroth tower right (upper)', 'Air vents left', 1337039, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), + LocationData('Varndagroth tower right (upper)', 'Air Vents right', 1337040, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), + LocationData('Varndagroth tower right (lower)', 'Right side bottom floor', 1337041), + LocationData('Varndagroth tower right (elevator)', 'Varndagroth', 1337042, lambda state: state._timespinner_has_keycard_C(world, player)), + LocationData('Varndagroth tower right (elevator)', 'Varndagroth Spider hell', 1337043, lambda state: state._timespinner_has_keycard_A(world, player)), + LocationData('Skeleton Shaft', 'Skeleton', 1337044), + LocationData('Sealed Caves (Xarion)', 'Shroom jump room', 1337045, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Sealed Caves (Xarion)', 'Double shroom room', 1337046), + LocationData('Sealed Caves (Xarion)', 'Mini jackpot room', 1337047, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Sealed Caves (Xarion)', 'Below mini jackpot room', 1337048), + LocationData('Sealed Caves (Xarion)', 'Sealed cave secret room', 1337049, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Sealed Caves (Xarion)', 'Below Sealed cave secret', 1337050), + LocationData('Sealed Caves (Xarion)', 'Last chance before Xarion', 1337051, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Sealed Caves (Xarion)', 'Xarion', 1337052), + LocationData('Sealed Caves (Sirens)', 'Solo siren chest', 1337053, lambda state: state.has('Water Mask', player)), + LocationData('Sealed Caves (Sirens)', 'Big siren room right', 1337054, lambda state: state.has('Water Mask', player)), + LocationData('Sealed Caves (Sirens)', 'Big siren Room left', 1337055, lambda state: state.has('Water Mask', player)), + LocationData('Sealed Caves (Sirens)', 'Room after sirens chest 2', 1337056), + LocationData('Sealed Caves (Sirens)', 'Room after sirens chest 1', 1337057), + LocationData('Militairy Fortress', 'Militairy Bomber chest', 1337058, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Militairy Fortress', 'Close combat room', 1337059), + LocationData('Militairy Fortress', 'Bridge full of soldiers', 1337060), + LocationData('Militairy Fortress', 'Giantess Room', 1337061), + LocationData('Militairy Fortress', 'Bridge with Giantess', 1337062), + LocationData('Militairy Fortress', 'Military B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), + LocationData('Militairy Fortress', 'Military B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), + LocationData('Militairy Fortress', 'Military pedestal', 1337065, lambda state: state._timespinner_has_doublejump(world, player) and (state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player))), + LocationData('The lab', 'Coffee Break chest', 1337066), + LocationData('The lab', 'Lower trash right', 1337067, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('The lab', 'Lower trash left', 1337068, lambda state: state._timespinner_has_upwarddash(world, player)), + LocationData('The lab', 'Single turret room', 1337069, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('The lab (power off)', 'Trash jump room', 1337070), + LocationData('The lab (power off)', 'Dynamo Works', 1337071), + LocationData('The lab (upper)', 'Blob mom', 1337072), + LocationData('The lab (power off)', 'Experiment #13', 1337073), + LocationData('The lab (upper)', 'Download and chest room', 1337074), + LocationData('The lab (upper)', 'Lab secret', 1337075, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('The lab (power off)', 'Lab Spider hell', 1337076, lambda state: state._timespinner_has_keycard_A(world, player)), + LocationData('Emperors tower', 'Bottom', 1337077), + LocationData('Emperors tower', 'After Courtyard Floor Secret', 1337078, lambda state: state._timespinner_has_upwarddash(world, player) and state._timespinner_can_break_walls(world, player)), + LocationData('Emperors tower', 'After Courtyard Chest', 1337079, lambda state: state._timespinner_has_upwarddash(world, player)), + LocationData('Emperors tower', 'Galactic Sage Room', 1337080), + LocationData('Emperors tower', 'Bottom of Right Tower', 1337081), + LocationData('Emperors tower', 'Wayyyy up there', 1337082), + LocationData('Emperors tower', 'Left tower balcony', 1337083), + LocationData('Emperors tower', 'Dad\'s Chambers chest', 1337084), + LocationData('Emperors tower', 'Dad\'s Chambers pedestal', 1337085), + + # PastItemLocations + LocationData('Refugee Camp', 'Neliste\'s Bra', 1337086), + LocationData('Refugee Camp', 'Refugee camp storage chest 3', 1337087), + LocationData('Refugee Camp', 'Refugee camp storage chest 2', 1337088), + LocationData('Refugee Camp', 'Refugee camp storage chest 1', 1337089), + LocationData('Forest', 'Refugee camp roof', 1337090), + LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Forest', 'Rats guarded chest', 1337093), + LocationData('Forest', 'Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), + LocationData('Forest', 'Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), + LocationData('Forest', 'Batcave', 1337096), + LocationData('Forest', 'Bridge Chest', 1337097), + LocationData('Left Side forest Caves', 'Solitary bat room', 1337098), + LocationData('Upper Lake Sirine', 'Rat nest', 1337099), + LocationData('Upper Lake Sirine', 'Double jump cave platform (past)', 1337100, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Upper Lake Sirine', 'Double jump cave floor (past)', 1337101), + LocationData('Upper Lake Sirine', 'West lake serene cave secret', 1337102, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Upper Lake Sirine', 'Chest behind vines', 1337103), + LocationData('Upper Lake Sirine', 'Pyramid keys room', 1337104), + LocationData('Lower Lake Sirine', 'Deep dive', 1337105), + LocationData('Lower Lake Sirine', 'Under the eels', 1337106), + LocationData('Lower Lake Sirine', 'Water spikes room', 1337107), + LocationData('Lower Lake Sirine', 'Underwater secret', 1337108, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Lower Lake Sirine', 'T chest', 1337109), + LocationData('Lower Lake Sirine', 'Past the eels', 1337110), + LocationData('Lower Lake Sirine', 'Underwater pedestal', 1337111), + LocationData('Caves of Banishment (upper)', 'Mushroom double jump', 1337112, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Caves of Banishment (upper)', 'Caves of banishment secret room', 1337113), + LocationData('Caves of Banishment (upper)', 'Below caves of banishment secret', 1337114), + LocationData('Caves of Banishment (upper)', 'Single shroom room', 1337115), + LocationData('Caves of Banishment (upper)', 'Jackpot room chest 1', 1337116, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Caves of Banishment (upper)', 'Jackpot room chest 2', 1337117, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Caves of Banishment (upper)', 'Jackpot room chest 3', 1337118, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Caves of Banishment (upper)', 'Jackpot room chest 4', 1337119, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Caves of Banishment (upper)', 'Banishment pedestal', 1337120), + LocationData('Caves of Banishment (Maw)', 'Last chance before Maw', 1337121, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player)), + LocationData('Caves of Banishment (Maw)', 'Mineshaft', 1337122, lambda state: state.has('Gas Mask', player)), + LocationData('Caves of Banishment (Sirens)', 'Wyvern room', 1337123), + LocationData('Caves of Banishment (Sirens)', 'Above water sirens', 1337124), + LocationData('Caves of Banishment (Sirens)', 'Underwater sirens left', 1337125, lambda state: state.has('Water Mask', player)), + LocationData('Caves of Banishment (Sirens)', 'Underwater sirens right', 1337126, lambda state: state.has('Water Mask', player)), + LocationData('Caves of Banishment (Sirens)', 'Water hook', 1337127), + LocationData('Caste Ramparts', 'Caste Bomber chest', 1337128, lambda state: state._timespinner_has_multiple_small_jumps_of_npc(world, player)), + LocationData('Caste Ramparts', 'Freeze the engineer', 1337129, lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player)), + LocationData('Caste Ramparts', 'Giantess guarded room', 1337130), + LocationData('Caste Ramparts', 'Knight and archer guarded room', 1337131), + LocationData('Caste Ramparts', 'Castle pedestal', 1337132), + LocationData('Caste Keep', 'Basement secret pedestal', 1337133, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Caste Keep', 'Break the wall', 1337134), + LocationData('Royal towers (lower)', 'Yas queen room', 1337135, lambda state: state._timespinner_has_pink(world, player)), + LocationData('Caste Keep', 'Basement hammer', 1337136), + LocationData('Caste Keep', 'Omelette chest', 1337137), + LocationData('Caste Keep', 'Just an egg', 1337138), + LocationData('Caste Keep', 'Out of the way', 1337139), + LocationData('Caste Keep', 'Killed Twins', EventId, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Caste Keep', 'Twins', 1337140, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Caste Keep', 'Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Royal towers (lower)', 'Royal tower floor secret', 1337142, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_can_break_walls(world, player)), + LocationData('Royal towers', 'Above the gap', 1337143), + LocationData('Royal towers', 'Under the ice mage', 1337144), + LocationData('Royal towers (upper)', 'Next to easy struggle juggle room', 1337145), + LocationData('Royal towers (upper)', 'Easy struggle juggle', 1337146, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Royal towers (upper)', 'Hard struggle juggle', 1337147, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Royal towers (upper)', 'No sturggle required', 1337148, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Royal towers', 'Right tower freebie', 1337149), + LocationData('Royal towers (upper)', 'Above the cide mage', 1337150), + LocationData('Royal towers (upper)', 'Royal guard big room', 1337151), + LocationData('Royal towers (upper)', 'Before Aelana', 1337152), + LocationData('Royal towers (upper)', 'Killed Aelana', EventId), + LocationData('Royal towers (upper)', 'Statue room', 1337153, lambda state: state._timespinner_has_upwarddash(world, player)), + LocationData('Royal towers (upper)', 'Aelana\'s pedestal', 1337154), + LocationData('Royal towers (upper)', 'After Aelana', 1337155), + + # 1337157 - 1337170 Downloads + + # 1337171 - 1337238 Reserved + + # PyramidItemLocations + #LocationData('Temporal Gyre', 'Transition chest 1', 1337239), + #LocationData('Temporal Gyre', 'Transition chest 2', 1337240), + #LocationData('Temporal Gyre', 'Transition chest 3', 1337241), + #LocationData('Temporal Gyre', 'Ravenlord pre fight', 1337242), + #LocationData('Temporal Gyre', 'Ravenlord post fight', 1337243), + #LocationData('Temporal Gyre', 'Ifrid pre fight', 1337244), + #LocationData('Temporal Gyre', 'Ifrid post fight', 1337245), + LocationData('Ancient Pyramid (left)', 'Why not it\'s right there', 1337246), + LocationData('Ancient Pyramid (left)', 'Conviction guarded room', 1337247), + LocationData('Ancient Pyramid (right)', 'Pit secret room', 1337248, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Ancient Pyramid (right)', 'Regret chest', 1337249, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId) + ) + + downloadable_items: Tuple[LocationData] = ( + # 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)), + 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)), + LocationData('Libary', 'V terminal 3', 1337162, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)), + LocationData('Libary top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)), + LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)), + LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)), + LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)), + LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Lab terminal right', 1337170, lambda state: state.has('Tablet', player)) + ) + + if not world or is_option_enabled(world, player, "DownloadableItems"): + return ( *location_table, *downloadable_items ) + else: + return location_table + +starter_progression_locations: Tuple[str] = ( + 'Starter chest 2', + 'Starter chest 3', + 'Starter chest 1', + 'Timespinner Wheel room' +) \ No newline at end of file diff --git a/worlds/timespinner/LogicMixin.py b/worlds/timespinner/LogicMixin.py new file mode 100644 index 00000000..8181b309 --- /dev/null +++ b/worlds/timespinner/LogicMixin.py @@ -0,0 +1,58 @@ +from BaseClasses import MultiWorld +from ..AutoWorld import LogicMixin +from .Options import is_option_enabled + +class TimespinnerLogic(LogicMixin): + def _timespinner_has_timestop(self, world: MultiWorld, player: int) -> bool: + return self.has_any(['Timespinner Wheel', 'Succubus Hairpin', 'Lightwall', 'Celestial Sash'], player) + + def _timespinner_has_doublejump(self, world: MultiWorld, player: int) -> bool: + return self.has_any(['Succubus Hairpin', 'Lightwall', 'Celestial Sash'], player) + + def _timespinner_has_forwarddash_doublejump(self, world: MultiWorld, player: int) -> bool: + return self._timespinner_has_upwarddash(world, player) or (self.has('Talaria Attachment', player) and self._timespinner_has_doublejump(world, player)) + + def _timespinner_has_doublejump_of_npc(self, world: MultiWorld, player: int) -> bool: + return self._timespinner_has_upwarddash(world, player) or (self.has('Timespinner Wheel', player) and self._timespinner_has_doublejump(world, player)) + + def _timespinner_has_multiple_small_jumps_of_npc(self, world: MultiWorld, player: int) -> bool: + return self.has('Timespinner Wheel', player) or self._timespinner_has_upwarddash(world, player) + + def _timespinner_has_upwarddash(self, world: MultiWorld, player: int) -> bool: + return self.has_any(['Lightwall', 'Celestial Sash'], player) + + def _timespinner_has_fire(self, world: MultiWorld, player: int) -> bool: + return self.has_any(['Fire Orb', 'Infernal Flames', 'Pyro Ring', 'Djinn Inferno'], player) + + def _timespinner_has_pink(self, world: MultiWorld, player: int) -> bool: + return self.has_any(['Plasma Orb', 'Plasma Geyser', 'Royal Ring'], player) + + def _timespinner_has_keycard_A(self, world: MultiWorld, player: int) -> bool: + return self.has('Security Keycard A', player) + + def _timespinner_has_keycard_B(self, world: MultiWorld, player: int) -> bool: + if is_option_enabled(world, player, "SpecificKeycards"): + return self.has('Security Keycard B', player) + else: + return self.has_any(['Security Keycard A', 'Security Keycard B'], player) + + def _timespinner_has_keycard_C(self, world: MultiWorld, player: int) -> bool: + if is_option_enabled(world, player, "SpecificKeycards"): + return self.has('Security Keycard C', player) + else: + return self.has_any(['Security Keycard A', 'Security Keycard B', 'Security Keycard C'], player) + + def _timespinner_has_keycard_D(self, world: MultiWorld, player: int) -> bool: + if is_option_enabled(world, player, "SpecificKeycards"): + return self.has('Security Keycard D', player) + else: + return self.has_any(['Security Keycard A', 'Security Keycard B', 'Security Keycard C', 'Security Keycard D'], player) + + def _timespinner_can_break_walls(self, world: MultiWorld, player: int) -> bool: + if is_option_enabled(world, player, "FacebookMode"): + return self.has('Oculus Ring', player) + else: + return True + + def _timespinner_can_kill_all_3_bosses(self, world: MultiWorld, player: int) -> bool: + return self.has_all(['Killed Maw', 'Killed Twins', 'Killed Aelana'], player) \ No newline at end of file diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py new file mode 100644 index 00000000..3ee636cc --- /dev/null +++ b/worlds/timespinner/Options.py @@ -0,0 +1,65 @@ +from typing import Dict +from BaseClasses import MultiWorld +from Options import Toggle + +class StartWithJewelryBox(Toggle): + "Start with Jewelry Box unlocked" + display_name = "Start with Jewelry Box" + +#class ProgressiveVerticalMovement(Toggle): +# "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash" +# display_name = "Progressive vertical movement" + +#class ProgressiveKeycards(Toggle): +# "Always find Security Keycard's in the following order D -> C -> B -> A" +# display_name = "Progressive keycards" + +class DownloadableItems(Toggle): + "With the tablet you will be able to download items at terminals" + display_name = "Downloadable items" + +class FacebookMode(Toggle): + "With the tablet you will be able to download items at terminals" + display_name = "Facebook mode" + +class StartWithMeyef(Toggle): + "Start with Meyef, ideal for when you want to play multiplayer." + display_name = "Start with Meyef" + +class QuickSeed(Toggle): + "Start with Talaria Attachment, Nyoom!" + display_name = "Quick seed" + +class SpecificKeycards(Toggle): + "Keycards can only open corresponding doors" + display_name = "Specific Keycards" + +class Inverted(Toggle): + "Start in the past" + display_name = "Inverted" + +#class StinkyMaw(Toggle): +# "Require gassmask for Maw" +# display_name = "Stinky Maw" + +# Some options that are available in the timespinner randomizer arent currently implemented +timespinner_options: Dict[str, Toggle] = { + "StartWithJewelryBox": StartWithJewelryBox, + #"ProgressiveVerticalMovement": ProgressiveVerticalMovement, + #"ProgressiveKeycards": ProgressiveKeycards, + "DownloadableItems": DownloadableItems, + "FacebookMode": FacebookMode, + "StartWithMeyef": StartWithMeyef, + "QuickSeed": QuickSeed, + "SpecificKeycards": SpecificKeycards, + "Inverted": Inverted, + #"StinkyMaw": StinkyMaw +} + +def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: + option = getattr(world, name, None) + + if option == None: + return False + + return int(option[player].value) > 0 \ No newline at end of file diff --git a/worlds/timespinner/PyramidKeys.py b/worlds/timespinner/PyramidKeys.py new file mode 100644 index 00000000..b9cb4368 --- /dev/null +++ b/worlds/timespinner/PyramidKeys.py @@ -0,0 +1,30 @@ +from typing import Tuple +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] = ( + "GateKittyBoss", + "GateLeftLibrary", + "GateMilitairyGate", + "GateSealedCaves", + "GateSealedSirensCave", + "GateLakeDesolation" + ) + + past_teleportation_gates: Tuple[str] = ( + "GateLakeSirineRight", + "GateAccessToPast", + "GateCastleRamparts", + "GateCastleKeep", + "GateRoyalTowers", + "GateMaw", + "GateCavesOfBanishment" + ) + + if is_option_enabled(world, player, "Inverted"): + gates = present_teleportation_gates + else: + gates = (*past_teleportation_gates, *present_teleportation_gates) + + return world.random.choice(gates) \ No newline at end of file diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py new file mode 100644 index 00000000..f3639927 --- /dev/null +++ b/worlds/timespinner/Regions.py @@ -0,0 +1,220 @@ +from typing import List, Dict, Tuple, Optional, Callable +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): + 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') + ] + + connectStartingRegion(world, player) + + names = {} + + connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player))) + connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Sirine', 'Region', player)) + connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Upper lake desolation', 'Lake desolation') + connect(world, player, names, 'Upper lake desolation', 'Lower lake desolation') + connect(world, player, names, 'Lower lake desolation', 'Lake desolation') + connect(world, player, names, 'Lower lake desolation', 'Libary') + connect(world, player, names, 'Lower lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Libary', 'Lower lake desolation') + connect(world, player, names, 'Libary', 'Libary top', lambda state: state._timespinner_has_doublejump(world, player) or state.has('Talaria Attachment', player)) + connect(world, player, names, 'Libary', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_C(world, player)) + connect(world, player, names, 'Libary', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Libary top', 'Libary') + connect(world, player, names, 'Varndagroth tower left', 'Libary') + connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (upper)', lambda state: state._timespinner_has_keycard_C(world, player)) + connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', lambda state: state._timespinner_has_keycard_B(world, player)) + connect(world, player, names, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: state._timespinner_has_keycard_B(world, player) and state.has('Elevator Keycard', player)) + connect(world, player, names, 'Varndagroth tower left', 'Refugee Camp', lambda state: state.has('Timespinner Wheel', player) and state.has('Timespinner Spindle', player)) + connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower left') + connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') + connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') + connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left') + connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: state._timespinner_has_keycard_B(world, player) and state.has('Elevator Keycard', player)) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Militairy Fortress', lambda state: state._timespinner_can_kill_all_3_bosses(world, player)) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower left', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) + connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Militairy Fortress', 'Varndagroth tower right (lower)', lambda state: state._timespinner_can_kill_all_3_bosses(world, player)) + connect(world, player, names, 'Militairy Fortress', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'The lab', 'Militairy Fortress') + connect(world, player, names, 'The lab', 'The lab (power off)', lambda state: state._timespinner_has_doublejump_of_npc(world, player)) + connect(world, player, names, 'The lab (power off)', 'The lab') + connect(world, player, names, 'The lab (power off)', 'The lab (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'The lab (upper)', 'The lab (power off)') + connect(world, player, names, 'The lab (upper)', 'Emperors tower', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (left)', lambda state: state.has_all(['Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'], player)) + connect(world, player, names, 'Emperors tower', 'The lab (upper)') + connect(world, player, names, 'Skeleton Shaft', 'Lake desolation') + connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player)) + connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft') + connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Refugee Camp', 'Forest') + connect(world, player, names, 'Refugee Camp', 'Libary', lambda state: is_option_enabled(world, player, "Inverted")) + connect(world, player, names, 'Refugee Camp', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Forest', 'Refugee Camp') + connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player)) + connect(world, player, names, 'Forest', 'Caves of Banishment (Sirens)') + connect(world, player, names, 'Forest', 'Caste Ramparts') + connect(world, player, names, 'Left Side forest Caves', 'Forest') + connect(world, player, names, 'Left Side forest Caves', 'Upper Lake Sirine', lambda state: state._timespinner_has_timestop(world, player)) + connect(world, player, names, 'Left Side forest Caves', 'Lower Lake Sirine', lambda state: state.has('Water Mask', player)) + connect(world, player, names, 'Left Side forest Caves', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Upper Lake Sirine', 'Left Side forest Caves') + connect(world, player, names, 'Upper Lake Sirine', 'Lower Lake Sirine', lambda state: state.has('Water Mask', player)) + connect(world, player, names, 'Lower Lake Sirine', 'Upper Lake Sirine') + connect(world, player, names, 'Lower Lake Sirine', 'Left Side forest Caves') + connect(world, player, names, 'Lower Lake Sirine', 'Caves of Banishment (upper)') + connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Sirine', lambda state: state.has('Water Mask', player)) + connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player)) + connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest') + connect(world, player, names, 'Caste Ramparts', 'Forest') + connect(world, player, names, 'Caste Ramparts', 'Caste Keep') + connect(world, player, names, 'Caste Ramparts', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Caste Keep', 'Caste Ramparts') + connect(world, player, names, 'Caste Keep', 'Royal towers (lower)', lambda state: state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Caste Keep', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Royal towers (lower)', 'Caste Keep') + connect(world, player, names, 'Royal towers (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or state._timespinner_has_forwarddash_doublejump(world, player)) + connect(world, player, names, 'Royal towers (lower)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Royal towers', 'Royal towers (lower)') + connect(world, player, names, 'Royal towers', 'Royal towers (upper)', lambda state: state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Royal towers (upper)', 'Royal towers') + connect(world, player, names, 'Ancient Pyramid (left)', 'The lab (upper)') + connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: state._timespinner_has_upwarddash(world, player)) + connect(world, player, names, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: state._timespinner_has_upwarddash(world, player)) + connect(world, player, names, 'Space time continuum', 'Lake desolation', lambda state: pyramid_keys_unlock == "GateLakeDesolation") + connect(world, player, names, 'Space time continuum', 'Lower lake desolation', lambda state: pyramid_keys_unlock == "GateKittyBoss") + connect(world, player, names, 'Space time continuum', 'Libary', lambda state: pyramid_keys_unlock == "GateLeftLibrary") + connect(world, player, names, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: pyramid_keys_unlock == "GateMilitairyGate") + connect(world, player, names, 'Space time continuum', 'Skeleton Shaft', lambda state: pyramid_keys_unlock == "GateSealedCaves") + connect(world, player, names, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: pyramid_keys_unlock == "GateSealedSirensCave") + connect(world, player, names, 'Space time continuum', 'Left Side forest Caves', lambda state: pyramid_keys_unlock == "GateLakeSirineRight") + connect(world, player, names, 'Space time continuum', 'Refugee Camp', lambda state: pyramid_keys_unlock == "GateAccessToPast") + connect(world, player, names, 'Space time continuum', 'Caste Ramparts', lambda state: pyramid_keys_unlock == "GateCastleRamparts") + connect(world, player, names, 'Space time continuum', 'Caste Keep', lambda state: pyramid_keys_unlock == "GateCastleKeep") + connect(world, player, names, 'Space time continuum', 'Royal towers (lower)', lambda state: pyramid_keys_unlock == "GateRoyalTowers") + 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: + location = Location(player, name, id, region) + location.access_rule = rule + + if id is None: + location.event = True + location.locked = True + + return location + +def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], 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) + region.locations.append(location) + + return region + +def connectStartingRegion(world: MultiWorld, player: int): + menu = world.get_region('Menu', player) + tutorial = world.get_region('Tutorial', player) + space_time_continuum = world.get_region('Space time continuum', player) + + if is_option_enabled(world, player, "Inverted"): + starting_region = world.get_region('Refugee Camp', player) + else: + starting_region = world.get_region('Lake desolation', player) + + menu_to_tutorial = Entrance(player, 'Tutorial', menu) + menu_to_tutorial.connect(tutorial) + menu.exits.append(menu_to_tutorial) + + tutorial_to_start = Entrance(player, 'Start Game', tutorial) + tutorial_to_start.connect(starting_region) + tutorial.exits.append(tutorial_to_start) + + teleport_back_to_start = Entrance(player, 'Teleport back to start', space_time_continuum) + 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) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, sourceRegion) + + if rule: + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) + +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 ] + + return per_region \ No newline at end of file diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py new file mode 100644 index 00000000..3bd19c15 --- /dev/null +++ b/worlds/timespinner/__init__.py @@ -0,0 +1,154 @@ +from typing import Dict, List +from BaseClasses import Item, MultiWorld +from ..AutoWorld import World +from .LogicMixin import TimespinnerLogic +from .Items import 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): + options = timespinner_options + game = "Timespinner" + topology_present = True + data_version = 1 + + 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)} + + locked_locations: Dict[int, List[str]] = {} + pyramid_keys_unlock: Dict[int, str] = {} + + def generate_early(self): + self.locked_locations[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]) + + 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"): + 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) + + fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], pool) + + 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["StinkyMaw"] = 1 + slot_data["ProgressiveVerticalMovement"] = 0 + slot_data["ProgressiveKeycards"] = 0 + slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player] + + return slot_data + +def create_item(name: str, player: int) -> Item: + data = item_table[name] + return Item(name, data.progression, data.code, player) + +def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[str]: + excluded_items = [] + + if is_option_enabled(world, player, "StartWithJewelryBox"): + excluded_items.append('Jewelry Box') + if is_option_enabled(world, player, "StartWithMeyef"): + excluded_items.append('Meyef') + if is_option_enabled(world, player, "QuickSeed"): + excluded_items.append('Talaria Attachment') + + return excluded_items + +def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]): + melee_weapon = world.random.choice(starter_melee_weapons) + spell = world.random.choice(starter_spells) + + excluded_items.append(melee_weapon) + excluded_items.append(spell) + + melee_weapon_item = create_item(melee_weapon, player) + spell_item = create_item(spell, player) + + world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item) + world.get_location('Yo Momma 2', player).place_locked_item(spell_item) + + locked_locations.append('Yo Momma 1') + locked_locations.append('Yo Momma 2') + +def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]: + pool = [] + + for name, data in item_table.items(): + if not name in excluded_items: + for _ in range(data.count): + item = update_progressive_state_based_flags(world, player, name, create_item(name, player)) + pool.append(item) + + return pool + +def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], pool: List[Item]): + for _ in range(len(get_locations(world, player)) - len(locked_locations) - len(pool)): + item = create_item(world.random.choice(filler_items), player) + pool.append(item) + +def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]): + progression_item = world.random.choice(starter_progression_items) + location = world.random.choice(starter_progression_locations) + + excluded_items.append(progression_item) + locked_locations.append(location) + + item = create_item(progression_item, player) + + world.get_location(location, player).place_locked_item(item) + +def update_progressive_state_based_flags(world: MultiWorld, player: int, name: str, data: Item) -> Item: + if not data.advancement: + return data + + if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"): + data.advancement = False + if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"): + data.advancement = False + + return data + +def setup_events(world: MultiWorld, player: int, locked_locations: List[str]): + for location in get_locations(world, player): + if location.code == EventId: + location = world.get_location(location.name, player) + item = Item(location.name, True, EventId, player) + + locked_locations.append(location.name) + + location.place_locked_item(item) + +def get_item_name_groups() -> Dict[str, List[str]]: + groups: Dict[str, List[str]] = {} + + for name, data in item_table.items(): + groups[data.category] = [ name ] if data.category not in groups else groups[data.category] + [ name ] + + return groups \ No newline at end of file