Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Subnautica Support #28

Merged
merged 1 commit into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions playerSettings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ game:
A Link to the Past: 1
Factorio: 1
Minecraft: 1
Subnautica: 1
requires:
version: 0.1.3 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
Expand Down
14 changes: 14 additions & 0 deletions worlds/subnautica/Items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import json

with open('worlds/subnautica/items.json', 'r') as file:
item_table = json.loads(file.read())

lookup_id_to_name = {}
lookup_name_to_item = {}
for item in item_table:
lookup_id_to_name[item["id"]] = item["name"]
lookup_name_to_item[item["name"]] = item

lookup_id_to_name[None] = "Victory"

lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
11 changes: 11 additions & 0 deletions worlds/subnautica/Locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import json

with open('worlds/subnautica/locations.json', 'r') as file:
location_table = json.loads(file.read())

lookup_id_to_name = {}
for item in location_table:
lookup_id_to_name[item["id"]] = item["name"]

lookup_id_to_name[None] = "Neptune Launch"
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}
8 changes: 8 additions & 0 deletions worlds/subnautica/Regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def create_regions(world, player: int):
from . import create_region
from .Locations import lookup_name_to_id as location_lookup_name_to_id

world.regions += [
create_region(world, player, 'Menu', None, ['Lifepod 5']),
create_region(world, player, 'Planet 4546B', [location for location in location_lookup_name_to_id])
]
254 changes: 254 additions & 0 deletions worlds/subnautica/Rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
from ..generic.Rules import set_rule
from .Locations import location_table
import logging
import math


def has_seaglide(state, player):
return state.has("Seaglide Fragment", player, 2)


def has_modification_station(state, player):
return state.has("Modification Station Fragment", player, 3)


def has_mobile_vehicle_bay(state, player):
return state.has("Mobile Vehicle Bay Fragment", player, 3)


def has_moonpool(state, player):
return state.has("Moonpool Fragment", player, 2)


def has_vehicle_upgrade_console(state, player):
return state.has("Vehicle Upgrade Console", player) and \
has_moonpool(state, player)


def has_seamoth(state, player):
return state.has("Seamoth Fragment", player, 3) and \
has_mobile_vehicle_bay(state, player)


def has_seamoth_depth_module_mk1(state, player):
return has_vehicle_upgrade_console(state, player)


def has_seamoth_depth_module_mk2(state, player):
return has_seamoth_depth_module_mk1(state, player) and \
has_modification_station(state, player)


def has_seamoth_depth_module_mk3(state, player):
return has_seamoth_depth_module_mk2(state, player) and \
has_modification_station(state, player)


def has_cyclops_bridge(state, player):
return state.has("Cyclops Bridge Fragment", player, 3)


def has_cyclops_engine(state, player):
return state.has("Cyclops Engine Fragment", player, 3)


def has_cyclops_hull(state, player):
return state.has("Cyclops Hull Fragment", player, 3)


def has_cyclops(state, player):
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):
return state.has("Cyclops Depth Module MK1", player) and \
has_modification_station(state, player)


def has_cyclops_depth_module_mk2(state, player):
return has_cyclops_depth_module_mk1(state, player) and \
has_modification_station(state, player)


def has_cyclops_depth_module_mk3(state, player):
return has_cyclops_depth_module_mk2(state, player) and \
has_modification_station(state, player)


def has_prawn(state, player):
return state.has("Prawn Suit Fragment", player, 4) and \
has_mobile_vehicle_bay(state, player)


def has_praw_propulsion_arm(state, player):
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):
return has_vehicle_upgrade_console(state, player)


def has_prawn_depth_module_mk2(state, player):
return has_prawn_depth_module_mk1(state, player) and \
has_modification_station(state, player)


def has_laser_cutter(state, player):
return state.has("Laser Cutter Fragment", player, 3)


# Either we have propulsion cannon, or prawn + propulsion cannon arm
def has_propulsion_cannon(state, player):
return state.has("Propulsion Cannon Fragment", player, 2) or \
(has_prawn(state, player) and has_praw_propulsion_arm(state, player))


def has_cyclops_shield(state, player):
return has_cyclops(state, player) and \
state.has("Cyclops Shield Generator", player)


# Swim depth rules:
# Rebreather, high capacity tank and fins are available from the start.
# All tests for those were done without inventory for light weight.
# Fins and ultra Fins are better than charge fins, so we ignore charge fins.
# We're ignoring lightweight tank in the chart, because the difference is
# negligeable with from high capacity tank. 430m -> 460m
# Fins are not used when using seaglide
#
def get_max_swim_depth(state, player):
#TODO, Make this a difficulty setting.
# Only go up to 200m without any submarines for now.
return 200

# Rules bellow, are what are technically possible

# has_ultra_high_capacity_tank = state.has("Ultra High Capacity Tank", player)
# has_ultra_glide_fins = state.has("Ultra Glide Fins", player)

# max_depth = 400 # More like 430m. Give some room
# if has_seaglide(state, player):
# if has_ultra_high_capacity_tank:
# max_depth = 750 # It's about 50m more. Give some room
# else:
# max_depth = 600 # It's about 50m more. Give some room
# elif has_ultra_high_capacity_tank:
# if has_ultra_glide_fins:
# pass
# else:
# pass
# elif has_ultra_glide_fins:
# max_depth = 500

# return max_depth


def get_seamoth_max_depth(state, player):
if has_seamoth(state, player):
if has_seamoth_depth_module_mk3(state, player):
return 900
elif has_seamoth_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
return 500
elif has_seamoth_depth_module_mk1(state, player):
return 300
else:
return 200
else:
return 0


def get_cyclops_max_depth(state, player):
if has_cyclops(state, player):
if has_cyclops_depth_module_mk3(state, player):
return 1700
elif has_cyclops_depth_module_mk2(state, player): # Will never be the case, 3 is craftable
return 1300
elif has_cyclops_depth_module_mk1(state, player):
return 900
else:
return 500
else:
return 0


def get_prawn_max_depth(state, player):
if has_prawn(state, player):
if has_prawn_depth_module_mk2(state, player):
return 1700
elif has_prawn_depth_module_mk1(state, player):
return 1300
else:
return 900
else:
return 0


def get_max_depth(state, player):
#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
return max(get_max_swim_depth(state, player), \
get_seamoth_max_depth(state, player), \
get_cyclops_max_depth(state, player), \
get_prawn_max_depth(state, player))


def can_access_location(state, player, loc):
# Extract location from game id.
# Game id is in format: "(x, y, z)"
pos_raw = loc.get("game_id")[1:-1].split(', ')
pos_x = float(pos_raw[0])
pos_y = float(pos_raw[1])
pos_z = float(pos_raw[2])
depth = -pos_y # y-up
map_center_dist = math.sqrt(pos_x**2 + pos_z**2)
aurora_dist = math.sqrt((pos_x - 1040)**2 + (pos_z - -160)**2)

need_radiation_suit = aurora_dist < 940
need_laser_cutter = loc.get("need_laser_cutter", False)
need_propulsion_cannon = loc.get("need_propulsion_cannon", False)

if need_laser_cutter and not has_laser_cutter(state, player):
return False

if need_radiation_suit and not state.has("Radiation Suit", player):
return False

if need_propulsion_cannon and not has_propulsion_cannon(state, player):
return False

# Seaglide doesn't unlock anything specific, but just allows for faster movement.
# Otherwise the game is painfully slow.
if (map_center_dist > 800 or pos_y < -200) and not has_seaglide(state, player):
return False

return get_max_depth(state, player) >= depth


def set_location_rule(world, player, loc):
set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc))


def set_rules(world, player):
logging.warning(type(location_table))
for loc in location_table:
set_location_rule(world, player, loc)

# Victory location
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 \
state.has('Neptune Gantry', player) and \
state.has('Neptune Boosters', player) and \
state.has('Neptune Fuel Reserve', player) and \
state.has('Neptune Cockpit', player) and \
state.has('Ion Power Cell', player) and \
state.has('Ion Battery', player) and \
has_cyclops_shield(state, player))

world.completion_condition[player] = lambda state: state.has('Victory', player)
89 changes: 89 additions & 0 deletions worlds/subnautica/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging
from typing import Set

logger = logging.getLogger("Subnautica")

from .Locations import lookup_name_to_id as locations_lookup_name_to_id
from .Items import item_table
from .Items import lookup_name_to_id as items_lookup_name_to_id

from .Regions import create_regions
from .Rules import set_rules

from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from ..AutoWorld import World


class SubnauticaWorld(World):
game: str = "Subnautica"
item_names: Set[str] = frozenset(items_lookup_name_to_id)
location_names: Set[str] = frozenset(locations_lookup_name_to_id)

item_name_to_id = items_lookup_name_to_id
location_name_to_id = locations_lookup_name_to_id

def generate_basic(self):
# Link regions
self.world.get_entrance('Lifepod 5', self.player).connect(self.world.get_region('Planet 4546B', self.player))

# Generate item pool
pool = []
neptune_launch_platform = None
for item in item_table:
for i in range(item["count"]):
subnautica_item = SubnauticaItem(item["name"], item["progression"], item["id"], player = self.player)
if item["name"] == "Neptune Launch Platform":
neptune_launch_platform = subnautica_item
else:
pool.append(subnautica_item)
self.world.itempool += pool

# Victory item
self.world.get_location("Aurora - Captain Data Terminal", self.player).place_locked_item(neptune_launch_platform)
self.world.get_location("Neptune Launch", self.player).place_locked_item(SubnauticaItem("Victory", True, None, player = self.player))


def set_rules(self):
set_rules(self.world, self.player)


def create_regions(self):
create_regions(self.world, self.player)


def generate_output(self, output_directory: str):
pass


def fill_slot_data(self):
slot_data = {}
return slot_data


def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = locations_lookup_name_to_id.get(location, 0)
location = SubnauticaLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))

return ret


class SubnauticaLocation(Location):
game: str = "Subnautica"

def __init__(self, player: int, name: str, address=None, parent=None):
super(SubnauticaLocation, self).__init__(player, name, address, parent)


class SubnauticaItem(Item):
game = "Subnautica"

def __init__(self, name, advancement, code, player: int = None):
super(SubnauticaItem, self).__init__(name, advancement, code, player)
Loading