Subnautica: revamp aggressive creature scans (#966)

* add forgotten aggressive creatures
* fix logic requirements
* added option to opt out of aggressive creature scans
This commit is contained in:
Fabian Dill 2022-08-30 17:14:34 +02:00 committed by GitHub
parent 4a2a184db1
commit 60d1a27079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import functools
from typing import Dict, Set, List from typing import Dict, Set, List
# EN Locale Creature Name to rough depth in meters found at # EN Locale Creature Name to rough depth in meters found at
@ -58,13 +59,18 @@ all_creatures: Dict[str, int] = {
aggressive: Set[str] = { aggressive: Set[str] = {
"Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency
"Crashfish", "Crashfish",
"Biter",
"Bleeder", "Bleeder",
"Blighter",
"Blood Crawler",
"Mesmer", "Mesmer",
"Reaper Leviathan", "Reaper Leviathan",
"Crabsquid", "Crabsquid",
"Warper", "Warper",
"Crabsnake", "Crabsnake",
"Ampeel", "Ampeel",
"Stalker",
"Sand Shark",
"Boneshark", "Boneshark",
"Lava Lizard", "Lava Lizard",
"Sea Dragon Leviathan", "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) 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()

View File

@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False):
need_propulsion_cannon: bool 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] = { location_table: Dict[int, LocationDict] = {
33000: {'can_slip_through': False, 33000: {'can_slip_through': False,

View File

@ -1,7 +1,7 @@
import typing import typing
from Options import Choice, Range, DeathLink from Options import Choice, Range, DeathLink
from .Creatures import all_creatures from .Creatures import all_creatures, Definitions
class ItemPool(Choice): class ItemPool(Choice):
@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice):
Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. 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. 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. 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: 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.""" Note: This is purely a logic expectation, and does not affect gameplay, only placement."""
display_name = "Aggressive Creature Scan Logic" display_name = "Aggressive Creature Scan Logic"
option_stasis = 0 option_stasis = 0
option_containment = 1 option_containment = 1
option_either = 2 option_either = 2
option_none = 3 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): class SubnauticaDeathLink(DeathLink):

View File

@ -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 worlds.generic.Rules import set_rule, add_rule
from .Locations import location_table, LocationDict 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 from .Options import AggressiveScanLogic
import math import math
@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location":
return 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]] = { aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
AggressiveScanLogic.option_stasis: has_stasis_rifle, AggressiveScanLogic.option_stasis: has_stasis_rifle,
AggressiveScanLogic.option_containment: has_containment, AggressiveScanLogic.option_containment: has_containment,
@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"):
set_location_rule(world, player, loc) set_location_rule(world, player, loc)
if subnautica_world.creatures_to_scan: 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: for creature_name in subnautica_world.creatures_to_scan:
location = set_creature_rule(world, player, creature_name) location = set_creature_rule(world, player, creature_name)
if creature_name in aggressive and aggressive_rule: if creature_name in containment: # there is no other way, hard-required containment
add_rule(location, lambda state: aggressive_rule(state, player)) 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 # 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 get_max_depth(state, player) >= 1444 and
has_mobile_vehicle_bay(state, player) and has_mobile_vehicle_bay(state, player) and
state.has("Neptune Launch Platform", player) and state.has("Neptune Launch Platform", player) and

View File

@ -52,14 +52,15 @@ class SubnauticaWorld(World):
self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment"),
self.create_item("Seaglide Fragment") self.create_item("Seaglide Fragment")
] ]
if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player]
valid_creatures = Creatures.all_creatures_presorted_without_containment creature_pool = scan_option.get_pool()
self.world.creature_scans[self.player].value = min(len(
Creatures.all_creatures_presorted_without_containment), self.world.creature_scans[self.player].value = min(
self.world.creature_scans[self.player].value) len(creature_pool),
else: self.world.creature_scans[self.player].value
valid_creatures = Creatures.all_creatures_presorted )
self.creatures_to_scan = self.world.random.sample(valid_creatures,
self.creatures_to_scan = self.world.random.sample(creature_pool,
self.world.creature_scans[self.player].value) self.world.creature_scans[self.player].value)
def create_regions(self): def create_regions(self):