From c695f91198f1094badfee98d15082024ebe00322 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Aug 2022 23:35:41 +0200 Subject: [PATCH] Subnautica: add Options to Creature Scans (#950) --- worlds/subnautica/Creatures.py | 25 +++++++-- worlds/subnautica/Items.py | 4 +- worlds/subnautica/Options.py | 20 ++++++- worlds/subnautica/Rules.py | 100 +++++++++++++++++++-------------- worlds/subnautica/__init__.py | 11 +++- 5 files changed, 109 insertions(+), 51 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 56e2a7ef..a9f5e850 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -50,10 +50,8 @@ all_creatures: Dict[str, int] = { "Lava Larva": 1300, "Lava Lizard": 1300, "Sea Dragon Leviathan": 1300, - "Sea Emperor Leviathan": 1700, + "Cuddlefish": 300, "Sea Emperor Juvenile": 1700, - - # "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes) } # be nice and make these require Stasis Rifle @@ -73,10 +71,29 @@ aggressive: Set[str] = { "River Prowler", } +containment: Set[str] = { # creatures that have to be raised from eggs + "Cuddlefish", +} + +hatchable: Set[str] = { # aggressive creatures that can be grown from eggs as alternative to stasis + "Ampeel", # warning: electric shocks + "Crabsquid", # warning: electric shocks + "Crabsnake", + "Boneshark", + "Crashfish", + "Gasopod", + "Lava Lizard", + "Mesmer", + "Sand Shark", + "Stalker", +} + suffix: str = " Scan" creature_locations: Dict[str, int] = { - creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) + creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) } all_creatures_presorted: List[str] = sorted(all_creatures) +all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] + diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 0f05d5e3..9917921a 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -42,7 +42,7 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Stillsuit', 'tech_type': 'Stillsuit'}, - 35008: {'classification': ItemClassification.filler, + 35008: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Alien Containment Fragment', 'tech_type': 'BaseWaterParkFragment'}, @@ -222,7 +222,7 @@ item_table: Dict[int, ItemDict] = { 'count': 2, 'name': 'Observatory Fragment', 'tech_type': 'BaseObservatoryFragment'}, - 35053: {'classification': ItemClassification.useful, + 35053: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Multipurpose Room', 'tech_type': 'BaseRoom'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f9f3f567..020b5d19 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Range, DeathLink +from Options import Choice, Range, DeathLink, Toggle from .Creatures import all_creatures @@ -33,12 +33,27 @@ class Goal(Choice): class CreatureScans(Range): - """Place items on specific creature scans. + """Place items on specific, randomly chosen, creature scans. Warning: Includes aggressive Leviathans.""" display_name = "Creature Scans" range_end = len(all_creatures) +class AggressiveScanLogic(Toggle): + """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle. + Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. + Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. + None: Aggressive Creatures are assumed to not need any tools to scan. + + Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" + display_name = "Aggressive Creature Scan Logic" + option_stasis = 0 + option_containment = 1 + option_either = 2 + option_none = 3 + + class SubnauticaDeathLink(DeathLink): """When you die, everyone dies. Of course the reverse is true too. Note: can be toggled via in-game console command "deathlink".""" @@ -48,5 +63,6 @@ options = { "item_pool": ItemPool, "goal": Goal, "creature_scans": CreatureScans, + "creature_scan_logic": AggressiveScanLogic, "death_link": SubnauticaDeathLink, } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index b8f8f1a7..20c6a35c 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,122 +1,128 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Callable -from worlds.generic.Rules import set_rule +from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict from .Creatures import all_creatures, aggressive, suffix +from .Options import AggressiveScanLogic import math if TYPE_CHECKING: from . import SubnauticaWorld + from BaseClasses import CollectionState, Location -def has_seaglide(state, player: int): +def has_seaglide(state: "CollectionState", player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player: int): +def has_modification_station(state: "CollectionState", player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player: int): +def has_mobile_vehicle_bay(state: "CollectionState", player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player: int): +def has_moonpool(state: "CollectionState", player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player: int): +def has_vehicle_upgrade_console(state: "CollectionState", player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player: int): +def has_seamoth(state: "CollectionState", player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player: int): +def has_seamoth_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player: int): +def has_seamoth_depth_module_mk2(state: "CollectionState", player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player: int): +def has_seamoth_depth_module_mk3(state: "CollectionState", player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player: int): +def has_cyclops_bridge(state: "CollectionState", player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player: int): +def has_cyclops_engine(state: "CollectionState", player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player: int): +def has_cyclops_hull(state: "CollectionState", player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player: int): +def has_cyclops(state: "CollectionState", player: int): 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, player: int): +def has_cyclops_depth_module_mk1(state: "CollectionState", player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player: int): +def has_cyclops_depth_module_mk2(state: "CollectionState", player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player: int): +def has_cyclops_depth_module_mk3(state: "CollectionState", player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player: int): +def has_prawn(state: "CollectionState", player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player: int): +def has_prawn_propulsion_arm(state: "CollectionState", player: int): return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state, player: int): +def has_prawn_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player: int): +def has_prawn_depth_module_mk2(state: "CollectionState", player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player: int): +def has_laser_cutter(state: "CollectionState", player: int): return state.has("Laser Cutter Fragment", player, 3) -def has_stasis_rile(state, player: int): +def has_stasis_rifle(state: "CollectionState", player: int): return state.has("Stasis Rifle Fragment", player, 2) +def has_containment(state: "CollectionState", player: int): + 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, player: int): +def has_propulsion_cannon(state: "CollectionState", player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ - (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) + (has_prawn(state, player) and has_prawn_propulsion_arm(state, player)) -def has_cyclops_shield(state, player: int): +def has_cyclops_shield(state: "CollectionState", player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -129,7 +135,7 @@ def has_cyclops_shield(state, player: int): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player: int): +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 @@ -140,7 +146,7 @@ def get_max_swim_depth(state, player: int): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player: int): + # 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: @@ -156,7 +162,7 @@ def get_max_swim_depth(state, player: int): # return max_depth -def get_seamoth_max_depth(state, player: int): +def get_seamoth_max_depth(state: "CollectionState", player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -170,7 +176,7 @@ def get_seamoth_max_depth(state, player: int): return 0 -def get_cyclops_max_depth(state, player): +def get_cyclops_max_depth(state: "CollectionState", player): if has_cyclops(state, player): if has_cyclops_depth_module_mk3(state, player): return 1700 @@ -184,7 +190,7 @@ def get_cyclops_max_depth(state, player): return 0 -def get_prawn_max_depth(state, player): +def get_prawn_max_depth(state: "CollectionState", player): if has_prawn(state, player): if has_prawn_depth_module_mk2(state, player): return 1700 @@ -196,7 +202,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player: int): +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 @@ -206,7 +212,7 @@ def get_max_depth(state, player: int): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict) -> bool: +def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): return False @@ -239,17 +245,25 @@ def set_location_rule(world, player: int, loc: LocationDict): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def can_scan_creature(state, player: int, creature: str) -> bool: +def can_scan_creature(state: "CollectionState", player: int, creature: str) -> bool: if not has_seaglide(state, player): return False - if creature in aggressive and not has_stasis_rile(state, player): - return False return get_max_depth(state, player) >= all_creatures[creature] -def set_creature_rule(world, player, creature_name: str): - set_rule(world.get_location(creature_name + suffix, player), +def set_creature_rule(world, player: int, creature_name: str) -> "Location": + location = world.get_location(creature_name + suffix, player) + set_rule(location, lambda state: can_scan_creature(state, player, creature_name)) + return location + + +aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { + AggressiveScanLogic.option_stasis: has_stasis_rifle, + AggressiveScanLogic.option_containment: has_containment, + AggressiveScanLogic.option_either: lambda state, player: + has_stasis_rifle(state, player) or has_containment(state, player) +} def set_rules(subnautica_world: "SubnauticaWorld"): @@ -259,8 +273,12 @@ def set_rules(subnautica_world: "SubnauticaWorld"): for loc in location_table.values(): set_location_rule(world, player, loc) - for creature_name in subnautica_world.creatures_to_scan: - set_creature_rule(world, player, creature_name) + if subnautica_world.creatures_to_scan: + aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + for creature_name in subnautica_world.creatures_to_scan: + location = set_creature_rule(world, player, creature_name) + if creature_name in aggressive and aggressive_rule: + add_rule(location, lambda state: aggressive_rule(state, player)) # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 6fa064d5..6562d93d 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations option_definitions = Options.options - data_version = 5 + data_version = 6 required_client_version = (0, 3, 4) prefill_items: List[Item] @@ -52,7 +52,14 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: + valid_creatures = Creatures.all_creatures_presorted_without_containment + self.world.creature_scans[self.player].value = min(len( + Creatures.all_creatures_presorted_without_containment), + self.world.creature_scans[self.player].value) + else: + valid_creatures = Creatures.all_creatures_presorted + self.creatures_to_scan = self.world.random.sample(valid_creatures, self.world.creature_scans[self.player].value) def create_regions(self):