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
# 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()

View File

@ -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,

View File

@ -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):

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 .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

View File

@ -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):