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:
parent
4a2a184db1
commit
60d1a27079
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue