Subnautica: add Options to Creature Scans (#950)
This commit is contained in:
parent
11cbc0b40b
commit
c695f91198
|
@ -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]
|
||||
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue