From 60d1a27079239bbcf08337e3322a4ab632480e2d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:14:34 +0200 Subject: [PATCH] Subnautica: revamp aggressive creature scans (#966) * add forgotten aggressive creatures * fix logic requirements * added option to opt out of aggressive creature scans --- worlds/subnautica/Creatures.py | 29 +++++++++++++++++++++++++++-- worlds/subnautica/Locations.py | 7 ++++++- worlds/subnautica/Options.py | 15 ++++++++++++++- worlds/subnautica/Rules.py | 28 ++++++++++++++++++++++------ worlds/subnautica/__init__.py | 17 +++++++++-------- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index a9f5e850..687c3732 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -1,3 +1,4 @@ +import functools from typing import Dict, Set, List # EN Locale Creature Name to rough depth in meters found at @@ -58,13 +59,18 @@ all_creatures: Dict[str, int] = { aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", + "Biter", "Bleeder", + "Blighter", + "Blood Crawler", "Mesmer", "Reaper Leviathan", "Crabsquid", "Warper", "Crabsnake", "Ampeel", + "Stalker", + "Sand Shark", "Boneshark", "Lava Lizard", "Sea Dragon Leviathan", @@ -94,6 +100,25 @@ creature_locations: Dict[str, int] = { 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] +class Definitions: + """Only compute lists if needed and then cache them.""" + + @functools.cached_property + def all_creatures_presorted(self) -> List[str]: + return sorted(all_creatures) + + @functools.cached_property + def all_creatures_presorted_without_containment(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in containment] + + @functools.cached_property + def all_creatures_presorted_without_stasis(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable] + + @functools.cached_property + def all_creatures_presorted_without_aggressive(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive] + +# only singleton needed +Definitions: Definitions = Definitions() diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 3effd1ea..2dfeaf3b 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False): need_propulsion_cannon: bool -events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] +events: List[str] = [ + "Neptune Launch", + "Disable Quarantine", + "Full Infection", + "Repair Aurora Drive", +] location_table: Dict[int, LocationDict] = { 33000: {'can_slip_through': False, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f68e12d2..57bd23fd 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,7 +1,7 @@ import typing from Options import Choice, Range, DeathLink -from .Creatures import all_creatures +from .Creatures import all_creatures, Definitions class ItemPool(Choice): @@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice): 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. + Removed: No Creatures needing Stasis or Containment will be in the pool at all. Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper. 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 + option_removed = 4 + + def get_pool(self) -> typing.List[str]: + if self == self.option_removed: + return Definitions.all_creatures_presorted_without_aggressive + elif self == self.option_stasis: + return Definitions.all_creatures_presorted_without_containment + elif self == self.option_containment: + return Definitions.all_creatures_presorted_without_stasis + else: + return Definitions.all_creatures_presorted class SubnauticaDeathLink(DeathLink): diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 20c6a35c..8925f1e8 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING, Dict, Callable +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 +from .Creatures import all_creatures, aggressive, suffix, hatchable, containment from .Options import AggressiveScanLogic import math @@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location": return location +def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \ + Optional[Callable[["CollectionState", int], bool]]: + """Get logic rule for a creature scan location.""" + if creature_name not in hatchable and option != option.option_none: # can only be done via stasis + return has_stasis_rifle + # otherwise allow option preference + return aggression_rules.get(option.value, None) + + aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { AggressiveScanLogic.option_stasis: has_stasis_rifle, AggressiveScanLogic.option_containment: has_containment, @@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(world, player, loc) if subnautica_world.creatures_to_scan: - aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + option = world.creature_scan_logic[player] + 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)) + if creature_name in containment: # there is no other way, hard-required containment + add_rule(location, lambda state: has_containment(state, player)) + elif creature_name in aggressive: + rule = get_aggression_rule(option, creature_name) + if rule: + add_rule(location, + lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player)) # Victory locations - set_rule(world.get_location("Neptune Launch", player), lambda state: + 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 diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 806c1b19..a4447ccb 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -52,14 +52,15 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - 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, + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] + creature_pool = scan_option.get_pool() + + self.world.creature_scans[self.player].value = min( + len(creature_pool), + self.world.creature_scans[self.player].value + ) + + self.creatures_to_scan = self.world.random.sample(creature_pool, self.world.creature_scans[self.player].value) def create_regions(self):