From f81d2653e0317398ad843a5b2aeb5ce44197dc30 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 28 Nov 2022 07:43:04 +0100 Subject: [PATCH] Subnautica: implement swim_rule option and skip goal prog balancing (#1258) --- worlds/subnautica/Items.py | 10 +-- worlds/subnautica/Options.py | 27 +++++++ worlds/subnautica/Rules.py | 151 ++++++++++++++++++----------------- 3 files changed, 110 insertions(+), 78 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 4a9eeabd..c9c886ac 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -186,7 +186,7 @@ item_table: Dict[int, ItemDict] = { 'count': 2, 'name': 'Propulsion Cannon Fragment', 'tech_type': 'PropulsionCannonFragment'}, - 35044: {'classification': ItemClassification.progression, + 35044: {'classification': ItemClassification.progression_skip_balancing, 'count': 1, 'name': 'Neptune Launch Platform', 'tech_type': 'RocketBase'}, @@ -314,19 +314,19 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Ion Battery', 'tech_type': 'PrecursorIonBattery'}, - 35076: {'classification': ItemClassification.progression, + 35076: {'classification': ItemClassification.progression_skip_balancing, 'count': 1, 'name': 'Neptune Gantry', 'tech_type': 'RocketBaseLadder'}, - 35077: {'classification': ItemClassification.progression, + 35077: {'classification': ItemClassification.progression_skip_balancing, 'count': 1, 'name': 'Neptune Boosters', 'tech_type': 'RocketStage1'}, - 35078: {'classification': ItemClassification.progression, + 35078: {'classification': ItemClassification.progression_skip_balancing, 'count': 1, 'name': 'Neptune Fuel Reserve', 'tech_type': 'RocketStage2'}, - 35079: {'classification': ItemClassification.progression, + 35079: {'classification': ItemClassification.progression_skip_balancing, 'count': 1, 'name': 'Neptune Cockpit', 'tech_type': 'RocketStage3'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 23bc09f2..8984fa86 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -4,6 +4,32 @@ from Options import Choice, Range, DeathLink, DefaultOnToggle from .Creatures import all_creatures, Definitions +class SwimRule(Choice): + """What logic considers ok swimming distances. + Easy: +200 depth from any max vehicle depth. + Normal: +400 depth from any max vehicle depth. + Warning: Normal can expect you to death run to a location (No viable return trip). + Hard: +600 depth from any max vehicle depth. + Warning: Hard may require bases, deaths, glitches, multi-tank inventory or other depth extending means. + Items: Expected depth is extended by items like seaglide, ultra glide fins and capacity tanks. + """ + display_name = "Swim Rule" + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_items_easy = 3 + option_items_normal = 4 + option_items_hard = 5 + + @property + def base_depth(self) -> int: + return [200, 400, 600][self.value % 3] + + @property + def consider_items(self) -> bool: + return self.value > 2 + + class EarlySeaglide(DefaultOnToggle): """Make sure 2 of the Seaglide Fragments are available in or near the Safe Shallows (Sphere 1 Locations).""" @@ -79,6 +105,7 @@ class SubnauticaDeathLink(DeathLink): options = { + "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, "item_pool": ItemPool, "goal": Goal, diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 544b9a2b..c978b6a1 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Dict, Callable, Optional from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict from .Creatures import all_creatures, aggressive, suffix, hatchable, containment -from .Options import AggressiveScanLogic +from .Options import AggressiveScanLogic, SwimRule import math if TYPE_CHECKING: @@ -11,155 +11,162 @@ if TYPE_CHECKING: from BaseClasses import CollectionState, Location -def has_seaglide(state: "CollectionState", player: int): +def has_seaglide(state: "CollectionState", player: int) -> bool: return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state: "CollectionState", player: int): +def has_modification_station(state: "CollectionState", player: int) -> bool: return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state: "CollectionState", player: int): +def has_mobile_vehicle_bay(state: "CollectionState", player: int) -> bool: return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state: "CollectionState", player: int): +def has_moonpool(state: "CollectionState", player: int) -> bool: return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state: "CollectionState", player: int): +def has_vehicle_upgrade_console(state: "CollectionState", player: int) -> bool: return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state: "CollectionState", player: int): +def has_seamoth(state: "CollectionState", player: int) -> bool: return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state: "CollectionState", player: int): +def has_seamoth_depth_module_mk1(state: "CollectionState", player: int) -> bool: return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state: "CollectionState", player: int): +def has_seamoth_depth_module_mk2(state: "CollectionState", player: int) -> bool: return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state: "CollectionState", player: int): +def has_seamoth_depth_module_mk3(state: "CollectionState", player: int) -> bool: return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state: "CollectionState", player: int): +def has_cyclops_bridge(state: "CollectionState", player: int) -> bool: return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state: "CollectionState", player: int): +def has_cyclops_engine(state: "CollectionState", player: int) -> bool: return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state: "CollectionState", player: int): +def has_cyclops_hull(state: "CollectionState", player: int) -> bool: return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state: "CollectionState", player: int): +def has_cyclops(state: "CollectionState", player: int) -> bool: return has_cyclops_bridge(state, player) and \ has_cyclops_engine(state, player) and \ has_cyclops_hull(state, player) and \ has_mobile_vehicle_bay(state, player) -def has_cyclops_depth_module_mk1(state: "CollectionState", player: int): +def has_cyclops_depth_module_mk1(state: "CollectionState", player: int) -> bool: return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state: "CollectionState", player: int): +def has_cyclops_depth_module_mk2(state: "CollectionState", player: int) -> bool: return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state: "CollectionState", player: int): +def has_cyclops_depth_module_mk3(state: "CollectionState", player: int) -> bool: return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state: "CollectionState", player: int): +def has_prawn(state: "CollectionState", player: int) -> bool: return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_prawn_propulsion_arm(state: "CollectionState", player: int): +def has_prawn_propulsion_arm(state: "CollectionState", player: int) -> bool: return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state: "CollectionState", player: int): +def has_prawn_depth_module_mk1(state: "CollectionState", player: int) -> bool: return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state: "CollectionState", player: int): +def has_prawn_depth_module_mk2(state: "CollectionState", player: int) -> bool: return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state: "CollectionState", player: int): +def has_laser_cutter(state: "CollectionState", player: int) -> bool: return state.has("Laser Cutter Fragment", player, 3) -def has_stasis_rifle(state: "CollectionState", player: int): +def has_stasis_rifle(state: "CollectionState", player: int) -> bool: return state.has("Stasis Rifle Fragment", player, 2) -def has_containment(state: "CollectionState", player: int): +def has_containment(state: "CollectionState", player: int) -> bool: return state.has("Alien Containment Fragment", player, 2) and state.has("Multipurpose Room", player) # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state: "CollectionState", player: int): - return state.has("Propulsion Cannon Fragment", player, 2) or \ - (has_prawn(state, player) and has_prawn_propulsion_arm(state, player)) +def has_propulsion_cannon(state: "CollectionState", player: int) -> bool: + return state.has("Propulsion Cannon Fragment", player, 2) -def has_cyclops_shield(state: "CollectionState", player: int): +def has_cyclops_shield(state: "CollectionState", player: int) -> bool: return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) +def has_ultra_high_capacity_tank(state: "CollectionState", player: int) -> bool: + return has_modification_station(state, player) and state.has("Ultra High Capacity Tank", player) + + +def has_lightweight_high_capacity_tank(state: "CollectionState", player: int) -> bool: + return has_modification_station(state, player) and state.has("Lightweight High Capacity Tank", player) + + +def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: + return has_modification_station(state, player) and state.has("Ultra Glide Fins", player) + # Swim depth rules: # Rebreather, high capacity tank and fins are available from the start. # All tests for those were done without inventory for light weight. # Fins and ultra Fins are better than charge fins, so we ignore charge fins. -# We're ignoring lightweight tank in the chart, because the difference is -# negligeable with from high capacity tank. 430m -> 460m -# Fins are not used when using seaglide -# -def get_max_swim_depth(state: "CollectionState", player: int): - # TODO, Make this a difficulty setting. - # Only go up to 200m without any submarines for now. - return 200 - # Rules bellow, are what are technically possible +# swim speeds: https://subnautica.fandom.com/wiki/Swimming_Speed - # has_ultra_high_capacity_tank = state.has("Ultra High Capacity Tank", player) - # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) - # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state: "CollectionState", player: int): - # if has_ultra_high_capacity_tank: - # max_depth = 750 # It's about 50m more. Give some room - # else: - # max_depth = 600 # It's about 50m more. Give some room - # elif has_ultra_high_capacity_tank: - # if has_ultra_glide_fins: - # pass - # else: - # pass - # elif has_ultra_glide_fins: - # max_depth = 500 - - # return max_depth +def get_max_swim_depth(state: "CollectionState", player: int) -> int: + swim_rule: SwimRule = state.multiworld.swim_rule[player] + depth: int = swim_rule.base_depth + if swim_rule == swim_rule.consider_items: + if has_seaglide(state, player): + if has_ultra_high_capacity_tank(state, player): + depth += 350 # It's about 800m. Give some room + else: + depth += 200 # It's about 650m. Give some room + # seaglide and fins cannot be used together + elif has_ultra_glide_fins(state, player): + if has_ultra_high_capacity_tank(state, player): + depth += 150 + elif has_lightweight_high_capacity_tank(state, player): + depth += 75 + else: + depth += 50 + elif has_ultra_high_capacity_tank(state, player): + depth += 100 + elif has_lightweight_high_capacity_tank(state, player): + depth += 25 + return depth def get_seamoth_max_depth(state: "CollectionState", player: int): @@ -203,13 +210,11 @@ def get_prawn_max_depth(state: "CollectionState", player): def get_max_depth(state: "CollectionState", player: int): - # TODO, Difficulty option, we can add vehicle depth + swim depth - # But at this point, we have to consider traver distance in caves, not - # just depth - return max(get_max_swim_depth(state, player), - get_seamoth_max_depth(state, player), - get_cyclops_max_depth(state, player), - get_prawn_max_depth(state, player)) + return get_max_swim_depth(state, player) + max( + get_seamoth_max_depth(state, player), + get_cyclops_max_depth(state, player), + get_prawn_max_depth(state, player) + ) def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool: @@ -298,22 +303,22 @@ def set_rules(subnautica_world: "SubnauticaWorld"): # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: - get_max_depth(state, player) >= 1444 and - has_mobile_vehicle_bay(state, player) and - state.has("Neptune Launch Platform", player) and - state.has("Neptune Gantry", player) and - state.has("Neptune Boosters", player) and - state.has("Neptune Fuel Reserve", player) and - state.has("Neptune Cockpit", player) and - state.has("Ion Power Cell", player) and - state.has("Ion Battery", player) and - has_cyclops_shield(state, player)) + get_max_depth(state, player) >= 1444 and + has_mobile_vehicle_bay(state, player) and + state.has("Neptune Launch Platform", player) and + state.has("Neptune Gantry", player) and + state.has("Neptune Boosters", player) and + state.has("Neptune Fuel Reserve", player) and + state.has("Neptune Cockpit", player) and + state.has("Ion Power Cell", player) and + state.has("Ion Battery", player) and + has_cyclops_shield(state, player)) set_rule(world.get_location("Disable Quarantine", player), lambda state: - get_max_depth(state, player) >= 1444) + get_max_depth(state, player) >= 1444) set_rule(world.get_location("Full Infection", player), lambda state: - get_max_depth(state, player) >= 900) + get_max_depth(state, player) >= 900) room = world.get_location("Aurora Drive Room - Upgrade Console", player) set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state))