From fc2e555b4adf6627b43289871db50d463939e46b Mon Sep 17 00:00:00 2001 From: Jarno Date: Sun, 19 Feb 2023 21:22:30 +0100 Subject: [PATCH] Timespinner: many new stuffs (#1433) * Timespinner: added RisingTides and DadPercent flags * Implemented logic for DadPercent and RisingTides * Fixed TODO's * Logic fixes * Fixed + removed LogicMixins * Fixes * More Fixes * Added UnchainedKeys flag * Fixed available items in pool with UnchainedKeys * Fixed typing callable * Fixed generation failures * More refactorings * Implemented traps * Fixed more typo * Fixed copy paste bug * Fixed teleporter logic * Fixed traps from pool * Fixed pyramid gates bug that causes a crash on connecting * Fixed seed reproduceability * Fixed logic eye for eye spy Now consider warp beacons as starter progression items * Attempt to add tracker icons using table * Replaced table layout with css grid * Fixed tracker + added Timespinner was apworld capatible * Updated archipelago items description * updated URL * Cleared up text * Fixed based on self review of PR * Fixed unit tests * Fixed seed reproduceability when the traps yaml option is not provided * Fixed logic for flooded basement * Implemented Beserkers review result I am not sure why, i guess this is just to make adding future games less conflicting? Co-authored-by: Fabian Dill * Added two new options (thanks to WeffJebster) * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Addition review results --------- Co-authored-by: Fabian Dill Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- .../static/styles/timespinnerTracker.css | 126 +++++++++-- WebHostLib/templates/timespinnerTracker.html | 139 ++++++------ setup.py | 1 + worlds/timespinner/Items.py | 24 +-- worlds/timespinner/Locations.py | 198 +++++++++--------- worlds/timespinner/LogicExtensions.py | 110 ++++++++++ worlds/timespinner/LogicMixin.py | 61 ------ worlds/timespinner/Options.py | 118 ++++++++++- worlds/timespinner/PreCalculatedWeights.py | 126 +++++++++++ worlds/timespinner/PyramidKeys.py | 33 --- worlds/timespinner/Regions.py | 162 +++++++------- worlds/timespinner/__init__.py | 148 ++++++++----- worlds/timespinner/docs/en_Timespinner.md | 4 +- 13 files changed, 840 insertions(+), 410 deletions(-) create mode 100644 worlds/timespinner/LogicExtensions.py delete mode 100644 worlds/timespinner/LogicMixin.py create mode 100644 worlds/timespinner/PreCalculatedWeights.py delete mode 100644 worlds/timespinner/PyramidKeys.py diff --git a/WebHostLib/static/styles/timespinnerTracker.css b/WebHostLib/static/styles/timespinnerTracker.css index e4e47f9e..007c6a19 100644 --- a/WebHostLib/static/styles/timespinnerTracker.css +++ b/WebHostLib/static/styles/timespinnerTracker.css @@ -9,19 +9,54 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; padding: 3px 3px 10px; - width: 384px; + width: 374px; background-color: #8d60a7; -} -#inventory-table td{ - width: 40px; - height: 40px; - text-align: center; - vertical-align: middle; + display: grid; + grid-template-rows: repeat(5, 48px); +} + +#inventory-table img{ + display: block; +} + +#inventory-table div.table-row{ + display: grid; + grid-template-columns: repeat(5, 1fr); +} + +#inventory-table div.C1{ + grid-column: 1; + place-content: center; + place-items: center; + display: flex; +} +#inventory-table div.C2{ + grid-column: 2; + place-content: center; + place-items: center; + display: flex; +} +#inventory-table div.C3{ + grid-column: 3; + place-content: center; + place-items: center; + display: flex; +} +#inventory-table div.C4{ + grid-column: 4; + place-content: center; + place-items: center; + display: flex; +} +#inventory-table div.C5{ + grid-column: 5; + place-content: center; + place-items: center; + display: flex; } #inventory-table img{ - height: 100%; max-width: 40px; max-height: 40px; filter: grayscale(100%) contrast(75%) brightness(30%); @@ -31,11 +66,70 @@ filter: none; } -#inventory-table div.counted-item { +#inventory-table img.acquired.purple{ /*00FFFF*/ + filter: hue-rotate(270deg) saturate(6) brightness(0.8); +} +#inventory-table img.acquired.cyan{ /*FF00FF*/ + filter: hue-rotate(138deg) saturate(10) brightness(0.8); +} +#inventory-table img.acquired.green{ /*32CD32*/ + filter: hue-rotate(84deg) saturate(10) brightness(0.7); +} + +#inventory-table div.image-stack{ + display: grid; + position: relative; + grid-template-columns: 1fr; + grid-template-rows: 1fr; +} + +#inventory-table div.image-stack div.stack-back{ + grid-column: 1; + grid-row: 1; +} + +#inventory-table div.image-stack div.stack-front{ + grid-column: 1; + grid-row: 1; + display: grid; + grid-template-columns: 20px 20px; + grid-template-rows: 20px 20px; +} + +#inventory-table div.image-stack div.stack-top-left{ + grid-column: 1; + grid-row: 1; + z-index: 1; +} + +#inventory-table div.image-stack div.stack-top-right{ + grid-column: 2; + grid-row: 1; + z-index: 1; +} + +#inventory-table div.image-stack div.stack-bottum-left{ + grid-column: 1; + grid-row: 2; + z-index: 1; +} + +#inventory-table div.image-stack div.stack-bottum-right{ + grid-column: 2; + grid-row: 2; + z-index: 1; +} + +#inventory-table div.image-stack div.stack-front img{ + width: 20px; + height: 20px; +} + +#inventory-table div.counted-item{ position: relative; } -#inventory-table div.item-count { +#inventory-table div.item-count{ position: absolute; color: white; font-family: "Minecraftia", monospace; @@ -69,16 +163,16 @@ line-height: 20px; } -#location-table td.counter { +#location-table td.counter{ text-align: right; font-size: 14px; } -#location-table td.toggle-arrow { +#location-table td.toggle-arrow{ text-align: right; } -#location-table tr#Total-header { +#location-table tr#Total-header{ font-weight: bold; } @@ -88,14 +182,14 @@ max-height: 30px; } -#location-table tbody.locations { +#location-table tbody.locations{ font-size: 12px; } -#location-table td.location-name { +#location-table td.location-name{ padding-left: 16px; } -.hide { +.hide{ display: none; } diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/timespinnerTracker.html index 82565316..f02ec6da 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/timespinnerTracker.html @@ -8,79 +8,94 @@
- - - - - - - - - - - - - - - - - - - - - {% if 'DownloadableItems' in options %} - - {% else %} - - {% endif %} - - - {% if 'DownloadableItems' in options %} - - {% else %} - - {% endif %} - - {% if 'EyeSpy' in options %} - - {% else %} - - {% endif %} - - - - - {% if 'GyreArchives' in options %} - - - {% else %} - - - {% endif %} - +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ {% if 'UnchainedKeys' in options %} + {% if 'EnterSandman' in options %} +
+ +
+ {% endif %} +
+ +
+
+ +
+ {% endif %} +
+
+
+
+
+
+
+
+
+ {% if 'DownloadableItems' in options %} +
+ {% endif %} +
+
+ {% if 'DownloadableItems' in options %} +
+ {% endif %} +
+ {% if 'EyeSpy' in options %} +
+ {% endif %} +
+
+
+
+ {% if 'GyreArchives' in options %} +
+
+ {% endif %} +
{% if 'Djinn Inferno' in acquired_items %} -
+ {% elif 'Pyro Ring' in acquired_items %} - + {% elif 'Fire Orb' in acquired_items %} - + {% elif 'Infernal Flames' in acquired_items %} - + {% else %} - + {% endif %} - + +
{% if 'Royal Ring' in acquired_items %} -
+ {% elif 'Plasma Geyser' in acquired_items %} - + {% elif 'Plasma Orb' in acquired_items %} - + {% else %} - + {% endif %} - -
+
+ + + {% for area in checks_done %} diff --git a/setup.py b/setup.py index d34798d3..382d3dc5 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ apworlds: set = { "Rogue Legacy", "Donkey Kong Country 3", "Super Mario World", + "Timespinner", } if os.path.exists("X:/pw.txt"): diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index c952faa6..add8beab 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -6,6 +6,7 @@ class ItemData(NamedTuple): count: int = 1 progression: bool = False useful: bool = False + trap: 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] = { @@ -190,7 +191,15 @@ item_table: Dict[str, ItemData] = { 'Hope Ring': ItemData('Orb Passive', 1337178), 'Max HP': ItemData('Stat', 1337179, 12), 'Max Aura': ItemData('Stat', 1337180, 13), - # 1337181 - 1337248 Reserved + 'Timeworn Warp Beacon': ItemData('Relic', 1337181, progression=True), + 'Modern Warp Beacon': ItemData('Relic', 1337182, progression=True), + 'Mysterious Warp Beacon': ItemData('Relic', 1337183, progression=True), + 'Meteor Sparrow Trap': ItemData('Trap', 1337184, 0, trap=True), + 'Poison Trap': ItemData('Trap', 1337185, 0, trap=True), + 'Chaos Trap': ItemData('Trap', 1337186, 0, trap=True), + 'Neurotoxin Trap': ItemData('Trap', 1337187, 0, trap=True), + 'Bee Trap': ItemData('Trap', 1337188, 0, trap=True), + # 1337189 - 1337248 Reserved 'Max Sand': ItemData('Stat', 1337249, 14) } @@ -230,19 +239,6 @@ starter_spells: Tuple[str, ...] = ( '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', diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index bdc3e380..960444ac 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,6 +1,8 @@ from typing import List, Tuple, Optional, Callable, NamedTuple -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from .Options import is_option_enabled +from .PreCalculatedWeights import PreCalculatedWeights +from .LogicExtensions import TimespinnerLogic EventId: Optional[int] = None @@ -9,10 +11,15 @@ class LocationData(NamedTuple): region: str name: str code: Optional[int] - rule: Callable = lambda state: True + rule: Callable[[CollectionState], bool] = lambda state: True -def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: +def get_locations(world: Optional[MultiWorld], player: Optional[int], + precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: + + flooded: PreCalculatedWeights = precalculated_weights + logic = TimespinnerLogic(world, player, precalculated_weights) + # 1337000 - 1337155 Generic locations # 1337171 - 1337175 New Pickup checks # 1337246 - 1337249 Ancient Pyramid @@ -24,13 +31,13 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Lake desolation', 'Lake Desolation: Starter chest 3', 1337003), LocationData('Lake desolation', 'Lake Desolation: Starter chest 1', 1337004), LocationData('Lake desolation', 'Lake Desolation (Lower): Timespinner Wheel room', 1337005), - LocationData('Lake desolation', 'Lake Desolation: Forget me not chest', 1337006, lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Serene', 'Region', player)), - LocationData('Lake desolation', 'Lake Desolation (Lower): Chicken chest', 1337007, lambda state: state._timespinner_has_timestop(world, player)), - LocationData('Lower lake desolation', 'Lake Desolation (Lower): Not so secret room', 1337008, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Lower lake desolation', 'Lake Desolation (Upper): Tank chest', 1337009, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Lake desolation', 'Lake Desolation: Forget me not chest', 1337006, lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)), + LocationData('Lake desolation', 'Lake Desolation (Lower): Chicken chest', 1337007, logic.has_timestop), + LocationData('Lower lake desolation', 'Lake Desolation (Lower): Not so secret room', 1337008, logic.can_break_walls), + LocationData('Lower lake desolation', 'Lake Desolation (Upper): Tank chest', 1337009, logic.has_timestop), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Oxygen recovery room', 1337010), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Secret room', 1337011, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Upper lake desolation', 'Lake Desolation (Upper): Double jump cave platform', 1337012, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Secret room', 1337011, logic.can_break_walls), + LocationData('Upper lake desolation', 'Lake Desolation (Upper): Double jump cave platform', 1337012, logic.has_doublejump), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Double jump cave floor', 1337013), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Sparrow chest', 1337014), LocationData('Upper lake desolation', 'Lake Desolation (Upper): Crash site pedestal', 1337015), @@ -41,9 +48,9 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Library', 'Library: Warp gate', 1337020), LocationData('Library', 'Library: Librarian', 1337021), LocationData('Library', 'Library: Reading nook chest', 1337022), - LocationData('Library', 'Library: Storage room chest 1', 1337023, lambda state: state._timespinner_has_keycard_D(world, player)), - LocationData('Library', 'Library: Storage room chest 2', 1337024, lambda state: state._timespinner_has_keycard_D(world, player)), - LocationData('Library', 'Library: Storage room chest 3', 1337025, lambda state: state._timespinner_has_keycard_D(world, player)), + LocationData('Library', 'Library: Storage room chest 1', 1337023, logic.has_keycard_D), + LocationData('Library', 'Library: Storage room chest 2', 1337024, logic.has_keycard_D), + LocationData('Library', 'Library: Storage room chest 3', 1337025, logic.has_keycard_D), LocationData('Library top', 'Library: Backer room chest 5', 1337026), LocationData('Library top', 'Library: Backer room chest 4', 1337027), LocationData('Library top', 'Library: Backer room chest 3', 1337028), @@ -51,59 +58,60 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Library top', 'Library: Backer room chest 1', 1337030), LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Elevator Key not required', 1337031), LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Ye olde Timespinner', 1337032), - LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Bottom floor', 1337033, lambda state: state._timespinner_has_keycard_C(world, player)), - LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Air vents secret', 1337034, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Bottom floor', 1337033, logic.has_keycard_C), + LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Air vents secret', 1337034, logic.can_break_walls), LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Elevator chest', 1337035, lambda state: state.has('Elevator Keycard', player)), LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers: Bridge', 1337036), LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Elevator chest', 1337037), - LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Elevator card chest', 1337038, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), - LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Air vents right chest', 1337039, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), - LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Air vents left chest', 1337040, lambda state: state.has('Elevator Keycard', player) or state._timespinner_has_doublejump(world, player)), + LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Elevator card chest', 1337038, lambda state: state.has('Elevator Keycard', player) or logic.has_doublejump(state)), + LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Air vents right chest', 1337039, lambda state: state.has('Elevator Keycard', player) or logic.has_doublejump(state)), + LocationData('Varndagroth tower right (upper)', 'Varndagroth Towers (Right): Air vents left chest', 1337040, lambda state: state.has('Elevator Keycard', player) or logic.has_doublejump(state)), LocationData('Varndagroth tower right (lower)', 'Varndagroth Towers (Right): Bottom floor', 1337041), - LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Varndagroth', 1337042, lambda state: state._timespinner_has_keycard_C(world, player)), - LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Spider Hell', 1337043, lambda state: state._timespinner_has_keycard_A(world, player)), + LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Varndagroth', 1337042, logic.has_keycard_C), + LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Spider Hell', 1337043, logic.has_keycard_A), LocationData('Skeleton Shaft', 'Sealed Caves (Xarion): Skeleton', 1337044), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Shroom jump room', 1337045, lambda state: state._timespinner_has_timestop(world, player)), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Shroom jump room', 1337045, logic.has_timestop), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Double shroom room', 1337046), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Mini jackpot room', 1337047, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Mini jackpot room', 1337047, logic.has_forwarddash_doublejump), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below mini jackpot room', 1337048), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, lambda state: state._timespinner_has_doublejump(world, player)), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Xarion', 1337052, lambda state: state.has('Water Mask', player) if flooded.flood_xarion else True), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Water hook', 1337053, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater right', 1337054, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Siren room underwater left', 1337055, lambda state: state.has('Water Mask', player)), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Cave after sirens chest 1', 1337056), LocationData('Sealed Caves (Sirens)', 'Sealed Caves (Sirens): Cave after sirens chest 2', 1337057), - LocationData('Military Fortress', 'Military Fortress: Bomber chest', 1337058, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Military Fortress', 'Military Fortress: Bomber chest', 1337058, lambda state: state.has('Timespinner Wheel', player) and logic.has_doublejump_of_npc(state)), LocationData('Military Fortress', 'Military Fortress: Close combat room', 1337059), LocationData('Military Fortress (hangar)', 'Military Fortress: Soldiers bridge', 1337060), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess room', 1337061), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess bridge', 1337062), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), - LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), + LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state)), LocationData('The lab', 'Lab: Coffee break', 1337066), - LocationData('The lab', 'Lab: Lower trash right', 1337067, lambda state: state._timespinner_has_doublejump(world, player)), - LocationData('The lab', 'Lab: Lower trash left', 1337068, lambda state: state._timespinner_has_upwarddash(world, player)), - LocationData('The lab', 'Lab: Below lab entrance', 1337069, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump), + LocationData('The lab', 'Lab: Lower trash left', 1337068, logic.has_upwarddash), + LocationData('The lab', 'Lab: Below lab entrance', 1337069, logic.has_doublejump), LocationData('The lab (power off)', 'Lab: Trash jump room', 1337070), LocationData('The lab (power off)', 'Lab: Dynamo Works', 1337071), LocationData('The lab (upper)', 'Lab: Genza (Blob Mom)', 1337072), LocationData('The lab (power off)', 'Lab: Experiment #13', 1337073), LocationData('The lab (upper)', 'Lab: Download and chest room chest', 1337074), - LocationData('The lab (upper)', 'Lab: 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('The lab (upper)', 'Lab: Lab secret', 1337075, logic.can_break_walls), + LocationData('The lab (power off)', 'Lab: Spider Hell', 1337076, logic.has_keycard_A), LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard bottom chest', 1337077), - LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: state._timespinner_has_upwarddash(world, player) and state._timespinner_can_break_walls(world, player)), - LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: state._timespinner_has_upwarddash(world, player)), + LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: logic.has_upwarddash(state) and logic.can_break_walls(state)), + LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: logic.has_upwarddash(state)), LocationData('Emperors tower', 'Emperor\'s Tower: Galactic sage room', 1337080), LocationData('Emperors tower', 'Emperor\'s Tower: Bottom right tower', 1337081), - LocationData('Emperors tower', 'Emperor\'s Tower: Wayyyy up there', 1337082, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Emperors tower', 'Emperor\'s Tower: Wayyyy up there', 1337082, logic.has_doublejump_of_npc), LocationData('Emperors tower', 'Emperor\'s Tower: Left tower balcony', 1337083), LocationData('Emperors tower', 'Emperor\'s Tower: Emperor\'s Chambers chest', 1337084), LocationData('Emperors tower', 'Emperor\'s Tower: Emperor\'s Chambers pedestal', 1337085), + LocationData('Emperors tower', 'Killed Emperor', EventId), # Past item locations LocationData('Refugee Camp', 'Refugee Camp: Neliste\'s Bra', 1337086), @@ -111,18 +119,18 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Refugee Camp', 'Refugee Camp: Storage chest 2', 1337088), LocationData('Refugee Camp', 'Refugee Camp: Storage chest 1', 1337089), LocationData('Forest', 'Forest: Refugee camp roof', 1337090), - LocationData('Forest', 'Forest: Bat jump ledge', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), - LocationData('Forest', 'Forest: Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Forest', 'Forest: Bat jump ledge', 1337091, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state) or logic.has_fastjump_on_npc(state)), + LocationData('Forest', 'Forest: Green platform secret', 1337092, logic.can_break_walls), LocationData('Forest', 'Forest: Rats guarded chest', 1337093), LocationData('Forest', 'Forest: Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Forest: Batcave', 1337096), - LocationData('Forest', 'Castle Ramparts: In the moat', 1337097), + LocationData('Forest', 'Castle Ramparts: In the moat', 1337097, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), LocationData('Left Side forest Caves', 'Forest: Before Serene single bat cave', 1337098), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Rat nest', 1337099), - LocationData('Upper Lake Serene', 'Lake Serene (Upper): Double jump cave platform', 1337100, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Upper Lake Serene', 'Lake Serene (Upper): Double jump cave platform', 1337100, logic.has_doublejump), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Double jump cave floor', 1337101), - LocationData('Upper Lake Serene', 'Lake Serene (Upper): Cave secret', 1337102, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Upper Lake Serene', 'Lake Serene (Upper): Cave secret', 1337102, logic.can_break_walls), LocationData('Upper Lake Serene', 'Lake Serene: Before Big Bird', 1337175), LocationData('Upper Lake Serene', 'Lake Serene: Behind the vines', 1337103), LocationData('Upper Lake Serene', 'Lake Serene: Pyramid keys room', 1337104), @@ -130,68 +138,68 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Lower Lake Serene', 'Lake Serene (Lower): Deep dive', 1337105), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: logic.has_doublejump_of_npc(state) if flooded.dry_lake_serene else True), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: state._timespinner_has_doublejump(world, player)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: logic.has_doublejump(state) if flooded.dry_lake_serene else True), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: logic.has_doublejump(state) if not flooded.flood_maw else True), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_maw else True)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: state._timespinner_has_forwarddash_doublejump(world, player)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state._timespinner_has_doublejump(world, player)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player)), - LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has('Gas Mask', player)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), + LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (state.has('Water Mask', player) if flooded.flood_maw else True)), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater right chest', 1337126, lambda state: state.has('Water Mask', player)), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater right ground', 1337172, lambda state: state.has('Water Mask', player)), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Water hook', 1337127, lambda state: state.has('Water Mask', player)), - LocationData('Castle Ramparts', 'Castle Ramparts: Bomber chest', 1337128, lambda state: state._timespinner_has_multiple_small_jumps_of_npc(world, player)), - LocationData('Castle Ramparts', 'Castle Ramparts: Freeze the engineer', 1337129, lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player)), + LocationData('Castle Ramparts', 'Castle Ramparts: Bomber chest', 1337128, logic.has_multiple_small_jumps_of_npc), + LocationData('Castle Ramparts', 'Castle Ramparts: Freeze the engineer', 1337129, lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)), LocationData('Castle Ramparts', 'Castle Ramparts: Giantess guarded room', 1337130), LocationData('Castle Ramparts', 'Castle Ramparts: Knight and archer guarded room', 1337131), LocationData('Castle Ramparts', 'Castle Ramparts: Pedestal', 1337132), - LocationData('Castle Keep', 'Castle Keep: Basement secret pedestal', 1337133, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Castle Keep', 'Castle Keep: Clean the castle basement', 1337134), - LocationData('Royal towers (lower)', 'Castle Keep: Yas queen room', 1337135, lambda state: state._timespinner_has_pink(world, player)), - LocationData('Castle Keep', 'Castle Keep: Giantess guarded chest', 1337136), - LocationData('Castle Keep', 'Castle Keep: Omelette chest', 1337137), - LocationData('Castle Keep', 'Castle Keep: Just an egg', 1337138), + LocationData('Castle Basement', 'Castle Basement: Secret pedestal', 1337133, logic.can_break_walls), + LocationData('Castle Basement', 'Castle Basement: Clean the castle basement', 1337134), + LocationData('Royal towers (lower)', 'Castle Keep: Yas queen room', 1337135, logic.has_pink), + LocationData('Castle Basement', 'Castle Basement: Giantess guarded chest', 1337136), + LocationData('Castle Basement', 'Castle Basement: Omelette chest', 1337137), + LocationData('Castle Basement', 'Castle Basement: Just an egg', 1337138), LocationData('Castle Keep', 'Castle Keep: Under the twins', 1337139), - LocationData('Castle Keep', 'Killed Twins', EventId, lambda state: state._timespinner_has_timestop(world, player)), - LocationData('Castle Keep', 'Castle Keep: Advisor jump', 1337171, lambda state: state._timespinner_has_timestop(world, player)), - LocationData('Castle Keep', 'Castle Keep: Twins', 1337140, lambda state: state._timespinner_has_timestop(world, player)), - LocationData('Castle Keep', 'Castle Keep: Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world,player)), - LocationData('Royal towers (lower)', 'Royal Towers: Floor secret', 1337142, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_can_break_walls(world, player)), + LocationData('Castle Keep', 'Killed Twins', EventId, logic.has_timestop), + LocationData('Castle Keep', 'Castle Keep: Advisor jump', 1337171, logic.has_timestop), + LocationData('Castle Keep', 'Castle Keep: Twins', 1337140, logic.has_timestop), + LocationData('Castle Keep', 'Castle Keep: Royal guard tiny room', 1337141, lambda state: logic.has_doublejump(state) or logic.has_fastjump_on_npc(state)), + LocationData('Royal towers (lower)', 'Royal Towers: Floor secret', 1337142, lambda state: logic.has_doublejump(state) and logic.can_break_walls(state)), LocationData('Royal towers', 'Royal Towers: Pre-climb gap', 1337143), - LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144), - LocationData('Royal towers (upper)', 'Royal Towers: Past bottom struggle juggle', 1337145), - LocationData('Royal towers (upper)', 'Royal Towers: Bottom struggle juggle', 1337146, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), - LocationData('Royal towers (upper)', 'Royal Towers: Top struggle juggle', 1337147, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), - LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Royal towers', 'Royal Towers: Long balcony', 1337144, lambda state: state.has('Water Mask', player) if flooded.flood_courtyard else True), + LocationData('Royal towers', 'Royal Towers: Past bottom struggle juggle', 1337145, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), + LocationData('Royal towers', 'Royal Towers: Bottom struggle juggle', 1337146, logic.has_doublejump_of_npc), + LocationData('Royal towers (upper)', 'Royal Towers: Top struggle juggle', 1337147, logic.has_doublejump_of_npc), + LocationData('Royal towers (upper)', 'Royal Towers: No struggle required', 1337148, logic.has_doublejump_of_npc), LocationData('Royal towers', 'Royal Towers: Right tower freebie', 1337149), LocationData('Royal towers (upper)', 'Royal Towers: Left tower small balcony', 1337150), LocationData('Royal towers (upper)', 'Royal Towers: Left tower royal guard', 1337151), LocationData('Royal towers (upper)', 'Royal Towers: Before Aelana', 1337152), LocationData('Royal towers (upper)', 'Killed Aelana', EventId), - LocationData('Royal towers (upper)', 'Royal Towers: Aelana\'s attic', 1337153, lambda state: state._timespinner_has_upwarddash(world, player)), + LocationData('Royal towers (upper)', 'Royal Towers: Aelana\'s attic', 1337153, logic.has_upwarddash), LocationData('Royal towers (upper)', 'Royal Towers: Aelana\'s chest', 1337154), LocationData('Royal towers (upper)', 'Royal Towers: Aelana\'s pedestal', 1337155), # Ancient pyramid locations LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236), - LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else True)), + LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: state.has('Water Mask', player) if flooded.flood_pyramid_back else True), + LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (state.has('Water Mask', player) if flooded.flood_pyramid_back else True)) ] # 1337156 - 1337170 Downloads @@ -205,7 +213,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library top', 'Library: Backer room terminal (Vandagray Metropolis Map)', 1337163, lambda state: state.has('Tablet', player)), - LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Medbay terminal (Bleakness Research)', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)), + LocationData('Varndagroth tower right (elevator)', 'Varndagroth Towers (Right): Medbay terminal (Bleakness Research)', 1337164, lambda state: state.has('Tablet', player) and logic.has_keycard_B(state)), LocationData('The lab (upper)', 'Lab: Download and chest room terminal (Experiment #13)', 1337165, lambda state: state.has('Tablet', player)), LocationData('The lab (power off)', 'Lab: Middle terminal (Amadeus Laboratory Map)', 1337166, lambda state: state.has('Tablet', player)), LocationData('The lab (power off)', 'Lab: Sentry platform terminal (Origins)', 1337167, lambda state: state.has('Tablet', player)), @@ -228,23 +236,23 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Library top', 'Library: Memory - Library Gap (Lachiemi Sun)', 1337179), LocationData('Library top', 'Library: Memory - Mr. Hat Portrait (Moonlit Night)', 1337180), LocationData('Varndagroth tower left', 'Varndagroth Towers (Left): Memory - Elevator (Nomads)', 1337181, lambda state: state.has('Elevator Keycard', player)), - LocationData('Varndagroth tower right (lower)', 'Varndagroth Towers: Memory - Siren Elevator (Childhood)', 1337182, lambda state: state._timespinner_has_keycard_B(world, player)), + LocationData('Varndagroth tower right (lower)', 'Varndagroth Towers: Memory - Siren Elevator (Childhood)', 1337182, logic.has_keycard_B), LocationData('Varndagroth tower right (lower)', 'Varndagroth Towers (Right): Memory - Bottom (Faron)', 1337183), - LocationData('Military Fortress', 'Military Fortress: Memory - Bomber Climb (A Solution)', 1337184, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)), - LocationData('The lab', 'Lab: Memory - Genza\'s Secret Stash 1 (An Old Friend)', 1337185, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('The lab', 'Lab: Memory - Genza\'s Secret Stash 2 (Twilight Dinner)', 1337186, lambda state: state._timespinner_can_break_walls(world, player)), - LocationData('Emperors tower', 'Emperor\'s Tower: Memory - Way Up There (Final Circle)', 1337187, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('Military Fortress', 'Military Fortress: Memory - Bomber Climb (A Solution)', 1337184, lambda state: state.has('Timespinner Wheel', player) and logic.has_doublejump_of_npc(state)), + LocationData('The lab', 'Lab: Memory - Genza\'s Secret Stash 1 (An Old Friend)', 1337185, logic.can_break_walls), + LocationData('The lab', 'Lab: Memory - Genza\'s Secret Stash 2 (Twilight Dinner)', 1337186, logic.can_break_walls), + LocationData('Emperors tower', 'Emperor\'s Tower: Memory - Way Up There (Final Circle)', 1337187, logic.has_doublejump_of_npc), LocationData('Forest', 'Forest: Journal - Rats (Lachiem Expedition)', 1337188), - LocationData('Forest', 'Forest: Journal - Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), - LocationData('Castle Ramparts', 'Castle Ramparts: Journal - Floating in Moat (Prime Edicts)', 1337190), + LocationData('Forest', 'Forest: Journal - Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state) or logic.has_fastjump_on_npc(state)), + LocationData('Forest', 'Forest: Journal - Floating in Moat (Prime Edicts)', 1337190, lambda state: state.has('Water Mask', player) if flooded.flood_moat else True), LocationData('Castle Ramparts', 'Castle Ramparts: Journal - Archer + Knight (Declaration of Independence)', 1337191), LocationData('Castle Keep', 'Castle Keep: Journal - Under the Twins (Letter of Reference)', 1337192), - LocationData('Castle Keep', 'Castle Keep: Journal - Castle Loop Giantess (Political Advice)', 1337193), - LocationData('Royal towers (lower)', 'Royal Towers: Journal - Aelana\'s Room (Diplomatic Missive)', 1337194, lambda state: state._timespinner_has_pink(world, player)), + LocationData('Castle Basement', 'Castle Basement: Journal - Castle Loop Giantess (Political Advice)', 1337193), + LocationData('Royal towers (lower)', 'Royal Towers: Journal - Aelana\'s Room (Diplomatic Missive)', 1337194, logic.has_pink), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196), - LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: state._timespinner_has_doublejump_of_npc(world, player)), - LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198) + LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: logic.has_doublejump_of_npc(state) if not flooded.flood_courtyard else True), + LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: state.has('Water Mask', player) if flooded.flood_maw else True) ) # 1337199 - 1337236 Reserved for future use @@ -264,11 +272,3 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L ) return tuple(location_table) - - -starter_progression_locations: Tuple[str, ...] = ( - 'Lake Desolation: Starter chest 2', - 'Lake Desolation: Starter chest 3', - 'Lake Desolation: Starter chest 1', - 'Lake Desolation (Lower): Timespinner Wheel room' -) diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py new file mode 100644 index 00000000..d316a936 --- /dev/null +++ b/worlds/timespinner/LogicExtensions.py @@ -0,0 +1,110 @@ +from typing import Union +from BaseClasses import MultiWorld, CollectionState +from .Options import is_option_enabled +from .PreCalculatedWeights import PreCalculatedWeights + + +class TimespinnerLogic: + player: int + + flag_unchained_keys: bool + flag_eye_spy: bool + flag_specific_keycards: bool + pyramid_keys_unlock: Union[str, None] + present_keys_unlock: Union[str, None] + past_keys_unlock: Union[str, None] + time_keys_unlock: Union[str, None] + + def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + self.player = player + + self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards") + self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy") + self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys") + + if precalculated_weights: + if self.flag_unchained_keys: + self.pyramid_keys_unlock = None + self.present_keys_unlock = precalculated_weights.present_key_unlock + self.past_keys_unlock = precalculated_weights.past_key_unlock + self.time_keys_unlock = precalculated_weights.time_key_unlock + else: + self.pyramid_keys_unlock = precalculated_weights.pyramid_keys_unlock + self.present_keys_unlock = None + self.past_keys_unlock = None + self.time_keys_unlock = None + + def has_timestop(self, state: CollectionState) -> bool: + return state.has_any({'Timespinner Wheel', 'Succubus Hairpin', 'Lightwall', 'Celestial Sash'}, self.player) + + def has_doublejump(self, state: CollectionState) -> bool: + return state.has_any({'Succubus Hairpin', 'Lightwall', 'Celestial Sash'}, self.player) + + def has_forwarddash_doublejump(self, state: CollectionState) -> bool: + return self.has_upwarddash(state) \ + or (state.has('Talaria Attachment', self.player) and self.has_doublejump(state)) + + def has_doublejump_of_npc(self, state: CollectionState) -> bool: + return self.has_upwarddash(state) \ + or (state.has('Timespinner Wheel', self.player) and self.has_doublejump(state)) + + def has_fastjump_on_npc(self, state: CollectionState) -> bool: + return state.has_all({'Timespinner Wheel', 'Talaria Attachment'}, self.player) + + def has_multiple_small_jumps_of_npc(self, state: CollectionState) -> bool: + return state.has('Timespinner Wheel', self.player) or self.has_upwarddash(state) + + def has_upwarddash(self, state: CollectionState) -> bool: + return state.has_any({'Lightwall', 'Celestial Sash'}, self.player) + + def has_fire(self, state: CollectionState) -> bool: + return state.has_any({'Fire Orb', 'Infernal Flames', 'Pyro Ring', 'Djinn Inferno'}, self.player) + + def has_pink(self, state: CollectionState) -> bool: + return state.has_any({'Plasma Orb', 'Plasma Geyser', 'Royal Ring'}, self.player) + + def has_keycard_A(self, state: CollectionState) -> bool: + return state.has('Security Keycard A', self.player) + + def has_keycard_B(self, state: CollectionState) -> bool: + if self.flag_specific_keycards: + return state.has('Security Keycard B', self.player) + else: + return state.has_any({'Security Keycard A', 'Security Keycard B'}, self.player) + + def has_keycard_C(self, state: CollectionState) -> bool: + if self.flag_specific_keycards: + return state.has('Security Keycard C', self.player) + else: + return state.has_any({'Security Keycard A', 'Security Keycard B', 'Security Keycard C'}, self.player) + + def has_keycard_D(self, state: CollectionState) -> bool: + if self.flag_specific_keycards: + return state.has('Security Keycard D', self.player) + else: + return state.has_any({'Security Keycard A', 'Security Keycard B', 'Security Keycard C', 'Security Keycard D'}, self.player) + + def can_break_walls(self, state: CollectionState) -> bool: + if self.flag_eye_spy: + return state.has('Oculus Ring', self.player) + else: + return True + + def can_kill_all_3_bosses(self, state: CollectionState) -> bool: + return state.has_all({'Killed Maw', 'Killed Twins', 'Killed Aelana'}, self.player) + + def has_teleport(self, state: CollectionState) -> bool: + return self.flag_unchained_keys or state.has('Twin Pyramid Key', self.player) + + def can_teleport_to(self, state: CollectionState, era: str, gate: str) -> bool: + if not self.flag_unchained_keys: + return self.pyramid_keys_unlock == gate + + if era == "Present": + return self.present_keys_unlock == gate and state.has("Modern Warp Beacon", self.player) + elif era == "Past": + return self.past_keys_unlock == gate and state.has("Timeworn Warp Beacon", self.player) + elif era == "Time": + return self.time_keys_unlock == gate and state.has("Mysterious Warp Beacon", self.player) + else: + raise Exception("Invallid Era: {}".format(era)) diff --git a/worlds/timespinner/LogicMixin.py b/worlds/timespinner/LogicMixin.py deleted file mode 100644 index 0bb8bf5d..00000000 --- a/worlds/timespinner/LogicMixin.py +++ /dev/null @@ -1,61 +0,0 @@ -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_fastjump_on_npc(self, world: MultiWorld, player: int) -> bool: - return self.has_all({'Timespinner Wheel', 'Talaria Attachment'}, 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, "EyeSpy"): - 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 index 81c68663..6f4b7ea8 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,6 +1,6 @@ -from typing import Dict, Union +from typing import Dict, Union, List from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList from schema import Schema, And, Optional @@ -191,6 +191,22 @@ class HpCap(Range): default = 999 +class LevelCap(Range): + """Sets the max level Lunais can achieve.""" + display_name = "Level Cap" + range_start = 1 + range_end = 99 + default = 99 + + +class ExtraEarringsXP(Range): + """Adds additional XP granted by Galaxy Earrings.""" + display_name = "Extra Earrings XP" + range_start = 0 + range_end = 24 + default = 0 + + class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" @@ -282,6 +298,94 @@ class EnterSandman(Toggle): display_name = "Enter Sandman" +class DadPercent(Toggle): + """The win condition is beating the boss of Emperor's Tower""" + display_name = "Dad Percent" + + +class RisingTides(Toggle): + """Random areas are flooded or drained, can be further specified with RisingTidesOverrides""" + display_name = "Rising Tides" + + +class RisingTidesOverrides(OptionDict): + """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. + Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + schema = Schema({ + Optional("Xarion"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("Maw"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("AncientPyramidShaft"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("Sandman"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("CastleMoat"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("CastleBasement"): { + "Dry": And(int, lambda n: n >= 0), + "FloodedWithSavePointAvailable": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("CastleCourtyard"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("LakeDesolation"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + }, + Optional("LakeSerene"): { + "Dry": And(int, lambda n: n >= 0), + "Flooded": And(int, lambda n: n >= 0) + } + }) + display_name = "Rising Tides Overrides" + default = { + "Xarion": { "Dry": 67, "Flooded": 33 }, + "Maw": { "Dry": 67, "Flooded": 33 }, + "AncientPyramidShaft": { "Dry": 67, "Flooded": 33 }, + "Sandman": { "Dry": 67, "Flooded": 33 }, + "CastleMoat": { "Dry": 67, "Flooded": 33 }, + "CastleBasement": { "Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17 }, + "CastleCourtyard": { "Dry": 67, "Flooded": 33 }, + "LakeDesolation": { "Dry": 67, "Flooded": 33 }, + "LakeSerene": { "Dry": 67, "Flooded": 33 }, + } + + +class UnchainedKeys(Toggle): + """Start with Twin Pyramid Key, which does not give free warp; + warp items for Past, Present, (and ??? with Enter Sandman) can be found.""" + display_name = "Unchained Keys" + + +class TrapChance(Range): + """Chance of traps in the item pool. + Traps will only replace filler items such as potions, vials and antidotes""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 10 + + +class Traps(OptionList): + """List of traps that may be in the item pool to find""" + display_name = "Traps Types" + valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } + default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] + + # Some options that are available in the timespinner randomizer arent currently implemented timespinner_options: Dict[str, Option] = { "StartWithJewelryBox": StartWithJewelryBox, @@ -299,6 +403,8 @@ timespinner_options: Dict[str, Option] = { "DamageRando": DamageRando, "DamageRandoOverrides": DamageRandoOverrides, "HpCap": HpCap, + "LevelCap": LevelCap, + "ExtraEarringsXP": ExtraEarringsXP, "BossHealing": BossHealing, "ShopFill": ShopFill, "ShopWarpShards": ShopWarpShards, @@ -310,6 +416,12 @@ timespinner_options: Dict[str, Option] = { "ShowBestiary": ShowBestiary, "ShowDrops": ShowDrops, "EnterSandman": EnterSandman, + "DadPercent": DadPercent, + "RisingTides": RisingTides, + "RisingTidesOverrides": RisingTidesOverrides, + "UnchainedKeys": UnchainedKeys, + "TrapChance": TrapChance, + "Traps": Traps, "DeathLink": DeathLink, } @@ -318,7 +430,7 @@ def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: return get_option_value(world, player, name) > 0 -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]: +def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: option = getattr(world, name, None) if option == None: return 0 diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py new file mode 100644 index 00000000..193bf84d --- /dev/null +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -0,0 +1,126 @@ +from typing import Tuple, Dict, Union +from BaseClasses import MultiWorld +from .Options import is_option_enabled, get_option_value + + +class PreCalculatedWeights: + pyramid_keys_unlock: str + present_key_unlock: str + past_key_unlock: str + time_key_unlock: str + + flood_basement: bool + flood_basement_high: bool + flood_xarion: bool + flood_maw: bool + flood_pyramid_shaft: bool + flood_pyramid_back: bool + flood_moat: bool + flood_courtyard: bool + flood_lake_desolation: bool + dry_lake_serene: bool + + def __init__(self, world: MultiWorld, player: int): + weights_overrrides: Dict[str, Dict[str, int]] = self.get_flood_weights_overrides(world, player) + + self.flood_basement, self.flood_basement_high = \ + self.roll_flood_setting_with_available_save(world, player, weights_overrrides, "CastleBasement") + self.flood_xarion = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") + self.flood_maw = self.roll_flood_setting(world, player, weights_overrrides, "Maw") + self.flood_pyramid_shaft = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") + self.flood_moat = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") + self.flood_courtyard = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") + self.dry_lake_serene = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + + self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ + self.get_pyramid_keys_unlock(world, player, self.flood_maw) + + + def get_pyramid_keys_unlock(self, world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: Tuple[str, ...] = ( + "GateKittyBoss", + "GateLeftLibrary", + "GateMilitaryGate", + "GateSealedCaves", + "GateSealedSirensCave", + "GateLakeDesolation" + ) + + past_teleportation_gates: Tuple[str, ...] = ( + "GateLakeSereneRight", + "GateAccessToPast", + "GateCastleRamparts", + "GateCastleKeep", + "GateRoyalTowers", + "GateCavesOfBanishment" + ) + + ancient_pyramid_teleportation_gates: Tuple[str, ...] = ( + "GateGyre", + "GateLeftPyramid", + "GateRightPyramid" + ) + + if not world: + return ( + present_teleportation_gates[0], + present_teleportation_gates[0], + past_teleportation_gates[0], + ancient_pyramid_teleportation_gates[0] + ) + + if not is_maw_flooded: + past_teleportation_gates += ("GateMaw", ) + + if is_option_enabled(world, player, "Inverted"): + all_gates: Tuple[str, ...] = present_teleportation_gates + else: + all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates + + return ( + world.random.choice(all_gates), + world.random.choice(present_teleportation_gates), + world.random.choice(past_teleportation_gates), + world.random.choice(ancient_pyramid_teleportation_gates) + ) + + @staticmethod + def get_flood_weights_overrides( world: MultiWorld, player: int) -> Dict[str, int]: + weights_overrides_option: Union[int, Dict[str, Dict[str, int]]] = \ + get_option_value(world, player, "RisingTidesOverrides") + + if weights_overrides_option == 0: + return {} + else: + return weights_overrides_option + + @staticmethod + def roll_flood_setting(world: MultiWorld, player: int, weights: Dict[str, Dict[str, int]], key: str) -> bool: + if not world or not is_option_enabled(world, player, "RisingTides"): + return False + + weights = weights[key] if key in weights else { "Dry": 67, "Flooded": 33 } + + result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + + return result == "Flooded" + + @staticmethod + def roll_flood_setting_with_available_save(world: MultiWorld, player: int, + weights: Dict[str, Dict[str, int]], key: str) -> Tuple[bool, bool]: + + if not world or not is_option_enabled(world, player, "RisingTides"): + return False, False + + weights = weights[key] if key in weights else {"Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17} + + result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + + if result == "Dry": + return False, False + elif result == "Flooded": + return True, False + elif result == "FloodedWithSavePointAvailable": + return True, True diff --git a/worlds/timespinner/PyramidKeys.py b/worlds/timespinner/PyramidKeys.py deleted file mode 100644 index 4e47b516..00000000 --- a/worlds/timespinner/PyramidKeys.py +++ /dev/null @@ -1,33 +0,0 @@ -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) - - if not world: - return gates[0] - - return world.random.choice(gates) \ No newline at end of file diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 5d1abe83..ab8ee97a 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,10 +1,14 @@ from typing import List, Set, Dict, Tuple, Optional, Callable -from BaseClasses import MultiWorld, Region, Entrance, Location +from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location from .Options import is_option_enabled from .Locations import LocationData +from .PreCalculatedWeights import PreCalculatedWeights +from .LogicExtensions import TimespinnerLogic -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str): +def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], + precalculated_weights: PreCalculatedWeights): + locations_per_region = get_locations_per_region(locations) regions = [ @@ -16,7 +20,6 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Eastern lake desolation'), create_region(world, player, locations_per_region, location_cache, 'Library'), create_region(world, player, locations_per_region, location_cache, 'Library top'), - create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'), 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)'), @@ -27,7 +30,6 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData 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, 'Ravenlord\'s Lair'), 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)'), @@ -42,6 +44,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'), create_region(world, player, locations_per_region, location_cache, 'Castle Ramparts'), create_region(world, player, locations_per_region, location_cache, 'Castle Keep'), + create_region(world, player, locations_per_region, location_cache, 'Castle Basement'), 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)'), @@ -52,6 +55,12 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Space time continuum') ] + if is_option_enabled(world, player, "GyreArchives"): + regions.extend([ + create_region(world, player, locations_per_region, location_cache, 'Ravenlord\'s Lair'), + create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'), + ]) + if __debug__: throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) @@ -59,125 +68,133 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connectStartingRegion(world, player) + flooded: PreCalculatedWeights = precalculated_weights + logic = TimespinnerLogic(world, player, precalculated_weights) names: Dict[str, int] = {} - 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 Serene', '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, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) + connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) + connect(world, player, names, 'Lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Upper lake desolation', 'Lake desolation') connect(world, player, names, 'Upper lake desolation', 'Eastern lake desolation') connect(world, player, names, 'Lower lake desolation', 'Lake desolation') connect(world, player, names, 'Lower lake desolation', 'Eastern lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Eastern lake desolation', 'Library') connect(world, player, names, 'Eastern lake desolation', 'Lower lake desolation') - connect(world, player, names, 'Eastern lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, names, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) connect(world, player, names, 'Library', 'Eastern lake desolation') - connect(world, player, names, 'Library', 'Library top', lambda state: state._timespinner_has_doublejump(world, player) or state.has('Talaria Attachment', player)) - connect(world, player, names, 'Library', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_D(world, player)) - connect(world, player, names, 'Library', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) + connect(world, player, names, 'Library', 'Varndagroth tower left', logic.has_keycard_D) + connect(world, player, names, 'Library', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Library top', 'Library') - connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) - connect(world, player, names, 'Ifrit\'s Lair', 'Library top') connect(world, player, names, 'Varndagroth tower left', 'Library') - 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', 'Varndagroth tower right (upper)', logic.has_keycard_C) + connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', logic.has_keycard_B) + connect(world, player, names, 'Varndagroth tower left', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) 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', lambda state: state._timespinner_has_keycard_B(world, player)) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left', logic.has_keycard_B) 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)', 'Military 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, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: logic.has_keycard_B(state) and state.has('Elevator Keycard', player)) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Military Fortress', logic.can_kill_all_3_bosses) + connect(world, player, names, 'Varndagroth tower right (lower)', 'Space time continuum', logic.has_teleport) 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, 'Military Fortress', 'Varndagroth tower right (lower)', lambda state: state._timespinner_can_kill_all_3_bosses(world, player)) + connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', logic.has_teleport) + connect(world, player, names, 'Military Fortress', 'Varndagroth tower right (lower)', logic.can_kill_all_3_bosses) connect(world, player, names, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) - connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', lambda state: state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) connect(world, player, names, 'Military Fortress (hangar)', 'Military Fortress') - connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) connect(world, player, names, 'Temporal Gyre', 'Military Fortress') connect(world, player, names, 'The lab', 'Military 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', 'The lab (power off)', logic.has_doublejump_of_npc) 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 (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) connect(world, player, names, 'The lab (upper)', 'The lab (power off)') - connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) - 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)', 'Emperors tower', logic.has_forwarddash_doublejump) connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) - connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)') 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, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) + connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) 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_doublejump(world, player)) - connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_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, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) + connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) + connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Refugee Camp', 'Forest') #connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not 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, 'Refugee Camp', 'Space time continuum', logic.has_teleport) 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', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) connect(world, player, names, 'Forest', 'Caves of Banishment (Sirens)') connect(world, player, names, 'Forest', 'Castle Ramparts') connect(world, player, names, 'Left Side forest Caves', 'Forest') - connect(world, player, names, 'Left Side forest Caves', 'Upper Lake Serene', lambda state: state._timespinner_has_timestop(world, player)) - connect(world, player, names, 'Left Side forest Caves', 'Lower Lake Serene', 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, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) + connect(world, player, names, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, names, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Upper Lake Serene', 'Left Side forest Caves') connect(world, player, names, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player)) connect(world, player, names, 'Lower Lake Serene', 'Upper Lake Serene') connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves') connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)') - connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', 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_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_doublejump(world, player)) + connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Twin Pyramid Key'}, player)) + connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) + connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', 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 (Maw)', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest') connect(world, player, names, 'Castle Ramparts', 'Forest') connect(world, player, names, 'Castle Ramparts', 'Castle Keep') - connect(world, player, names, 'Castle Ramparts', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Castle Keep', 'Castle Ramparts') - connect(world, player, names, 'Castle Keep', 'Royal towers (lower)', lambda state: state._timespinner_has_doublejump(world, player)) - connect(world, player, names, 'Castle Keep', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) + connect(world, player, names, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) + connect(world, player, names, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) + connect(world, player, names, 'Castle Keep', 'Space time continuum', logic.has_teleport) connect(world, player, names, 'Royal towers (lower)', 'Castle 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 (lower)', 'Royal towers', lambda state: state.has('Timespinner Wheel', player) or logic.has_forwarddash_doublejump(state)) + connect(world, player, names, 'Royal towers (lower)', 'Space time continuum', logic.has_teleport) 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', 'Royal towers (upper)', logic.has_doublejump) connect(world, player, names, 'Royal towers (upper)', 'Royal towers') - connect(world, player, names, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) - connect(world, player, names, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', lambda state: state._timespinner_has_doublejump(world, player)) + #connect(world, player, names, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) + connect(world, player, names, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') - 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', 'Library', 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', 'Castle Ramparts', lambda state: pyramid_keys_unlock == "GateCastleRamparts") - connect(world, player, names, 'Space time continuum', 'Castle 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") - connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: is_option_enabled(world, player, "EnterSandman")) + connect(world, player, names, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, names, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, names, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) + connect(world, player, names, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) + connect(world, player, names, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) + connect(world, player, names, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) + connect(world, player, names, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) + connect(world, player, names, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) + connect(world, player, names, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) + connect(world, player, names, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) + connect(world, player, names, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) + connect(world, player, names, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) + connect(world, player, names, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) + connect(world, player, names, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) + connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) + connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) + connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) + connect(world, player, names, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) + + if is_option_enabled(world, player, "GyreArchives"): + connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) + connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)') + connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) + connect(world, player, names, 'Ifrit\'s Lair', 'Library top') def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): - existingRegions = set() + existingRegions: Set[str] = set() for region in regions: existingRegions.add(region.name) @@ -233,7 +250,8 @@ def connectStartingRegion(world: MultiWorld, player: int): space_time_continuum.exits.append(teleport_back_to_start) -def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None): +def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, + rule: Optional[Callable[[CollectionState], bool]] = None): sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index ffe89bf5..cb52459b 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,15 +1,11 @@ from typing import Dict, List, Set, Tuple, TextIO - from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification -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 .LogicMixin import TimespinnerLogic +from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, filler_items +from .Locations import get_locations, EventId from .Options import is_option_enabled, get_option_value, timespinner_options -from .PyramidKeys import get_pyramid_keys_unlock +from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions -from ..AutoWorld import World, WebWorld - +from worlds.AutoWorld import World, WebWorld class TimespinnerWebWorld(WebWorld): theme = "ice" @@ -43,23 +39,24 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 10 + data_version = 11 web = TimespinnerWebWorld() + required_client_version = (0, 3, 7) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_locations(None, None)} + location_name_to_id = {location.name: location.code for location in get_locations(None, None, None)} item_name_groups = get_item_names_per_category() locked_locations: List[str] - pyramid_keys_unlock: str location_cache: List[Location] + precalculated_weights: PreCalculatedWeights def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) self.locked_locations = [] self.location_cache = [] - self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player) + self.precalculated_weights = PreCalculatedWeights(world, player) def generate_early(self): # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly @@ -71,28 +68,37 @@ class TimespinnerWorld(World): self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true def create_regions(self): - create_regions(self.multiworld, self.player, get_locations(self.multiworld, self.player), - self.location_cache, self.pyramid_keys_unlock) + locations = get_locations(self.multiworld, self.player, self.precalculated_weights) + create_regions(self.multiworld, self.player, locations, self.location_cache, self.precalculated_weights) def create_item(self, name: str) -> Item: return create_item_with_correct_settings(self.multiworld, self.player, name) def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(filler_items) + trap_chance: int = get_option_value(self.multiworld, self.player, "TrapChance") + enabled_traps: List[str] = get_option_value(self.multiworld, self.player, "Traps") + + if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: + return self.multiworld.random.choice(enabled_traps) + else: + return self.multiworld.random.choice(filler_items) def set_rules(self): setup_events(self.player, self.locked_locations, self.location_cache) - self.multiworld.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player) + final_boss: str + if is_option_enabled(self.multiworld, self.player, "DadPercent"): + final_boss = "Killed Emperor" + else: + final_boss = "Killed Nightmare" + + self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) def generate_basic(self): - excluded_items = get_excluded_items(self, self.multiworld, self.player) + excluded_items: Set[str] = get_excluded_items(self, self.multiworld, self.player) assign_starter_items(self.multiworld, self.player, excluded_items, self.locked_locations) - if not is_option_enabled(self.multiworld, self.player, "QuickSeed") and not is_option_enabled(self.multiworld, self.player, "Inverted"): - place_first_progression_item(self.multiworld, self.player, excluded_items, self.locked_locations) - pool = get_item_pool(self.multiworld, self.player, excluded_items) fill_item_pool_with_dummy_items(self, self.multiworld, self.player, self.locked_locations, self.location_cache, pool) @@ -102,19 +108,74 @@ class TimespinnerWorld(World): def fill_slot_data(self) -> Dict[str, object]: slot_data: Dict[str, object] = {} + ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"} + for option_name in timespinner_options: - slot_data[option_name] = get_option_value(self.multiworld, self.player, option_name) + if (option_name not in ap_specific_settings): + slot_data[option_name] = get_option_value(self.multiworld, self.player, option_name) slot_data["StinkyMaw"] = True slot_data["ProgressiveVerticalMovement"] = False slot_data["ProgressiveKeycards"] = False - slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache) + slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock + slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock + slot_data["PastGate"] = self.precalculated_weights.past_key_unlock + slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock + slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \ + int(self.precalculated_weights.flood_basement_high) + slot_data["Xarion"] = self.precalculated_weights.flood_xarion + slot_data["Maw"] = self.precalculated_weights.flood_maw + slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft + slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back + slot_data["CastleMoat"] = self.precalculated_weights.flood_moat + slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard + slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation + slot_data["DryLakeSerene"] = self.precalculated_weights.dry_lake_serene return slot_data def write_spoiler_header(self, spoiler_handle: TextIO): - spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock)) + if is_option_enabled(self.multiworld, self.player, "UnchainedKeys"): + spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') + spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') + + if is_option_enabled(self.multiworld, self.player, "EnterSandman"): + spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') + else: + spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') + + if is_option_enabled(self.multiworld, self.player, "RisingTides"): + flooded_areas: List[str] = [] + + if self.precalculated_weights.flood_basement: + if self.precalculated_weights.flood_basement_high: + flooded_areas.append("Castle Basement") + else: + flooded_areas.append("Castle Basement (Savepoint available)") + if self.precalculated_weights.flood_xarion: + flooded_areas.append("Xarion (boss)") + if self.precalculated_weights.flood_maw: + flooded_areas.append("Maw (caves + boss)") + if self.precalculated_weights.flood_pyramid_shaft: + flooded_areas.append("Ancient Pyramid Shaft") + if self.precalculated_weights.flood_pyramid_back: + flooded_areas.append("Sandman\\Nightmare (boss)") + if self.precalculated_weights.flood_moat: + flooded_areas.append("Castle Ramparts Moat") + if self.precalculated_weights.flood_courtyard: + flooded_areas.append("Castle Courtyard") + if self.precalculated_weights.flood_lake_desolation: + flooded_areas.append("Lake Desolation") + if self.precalculated_weights.dry_lake_serene: + flooded_areas.append("Dry Lake Serene") + + if len(flooded_areas) == 0: + flooded_areas_string: str = "None" + else: + flooded_areas_string: str = ", ".join(flooded_areas) + + spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]: @@ -127,6 +188,16 @@ def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) - if is_option_enabled(world, player, "QuickSeed"): excluded_items.add('Talaria Attachment') + if is_option_enabled(world, player, "UnchainedKeys"): + excluded_items.add('Twin Pyramid Key') + + if not is_option_enabled(world, player, "EnterSandman"): + excluded_items.add('Mysterious Warp Beacon') + else: + excluded_items.add('Timeworn Warp Beacon') + excluded_items.add('Modern Warp Beacon') + excluded_items.add('Mysterious Warp Beacon') + for item in world.precollected_items[player]: if item.name not in self.item_name_groups['UseItem']: excluded_items.add(item.name) @@ -188,38 +259,18 @@ def fill_item_pool_with_dummy_items(self: TimespinnerWorld, world: MultiWorld, p pool.append(item) -def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): - for item in world.precollected_items[player]: - if item.name in starter_progression_items: - return - - local_starter_progression_items = tuple( - item for item in starter_progression_items if item not in world.non_local_items[player].value) - non_excluded_starter_progression_locations = tuple( - location for location in starter_progression_locations if location not in world.exclude_locations[player].value) - - if not local_starter_progression_items or not non_excluded_starter_progression_locations: - return - - progression_item = world.random.choice(local_starter_progression_items) - location = world.random.choice(non_excluded_starter_progression_locations) - - excluded_items.add(progression_item) - locked_locations.append(location) - - item = create_item_with_correct_settings(world, player, progression_item) - - world.get_location(location, player).place_locked_item(item) - - def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item: data = item_table[name] + if data.useful: classification = ItemClassification.useful elif data.progression: classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap else: classification = ItemClassification.filler + item = Item(name, classification, data.code, player) if not item.advancement: @@ -231,6 +282,9 @@ def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) item.classification = ItemClassification.filler elif (name == 'Kobo' or name == 'Merchant Crow') and not is_option_enabled(world, player, "GyreArchives"): item.classification = ItemClassification.filler + elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ + and not is_option_enabled(world, player, "UnchainedKeys"): + item.classification = ItemClassification.filler return item diff --git a/worlds/timespinner/docs/en_Timespinner.md b/worlds/timespinner/docs/en_Timespinner.md index f0e69729..6a9e7fa4 100644 --- a/worlds/timespinner/docs/en_Timespinner.md +++ b/worlds/timespinner/docs/en_Timespinner.md @@ -28,9 +28,7 @@ certain items to your own world. ## What does another world's item look like in Timespinner? -Items belonging to other worlds are represented by the vanilla item Elemental -Beads ([Elemental Beads Wiki Page](https://timespinnerwiki.com/Use_Items)), Elemental Beads have no use in the -randomizer. +Items belonging to other worlds are represented by the four orbs icon from the [Orb Collectors (Wiki Page)](https://timespinnerwiki.com/Achievements) achievement. ## When the player receives an item, what happens?