diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 9d7b7f4959d..6f978211fb7 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -154,8 +154,10 @@ def autogen(config: dict): while 1: time.sleep(0.1) with db_session: + # for update locks the database row(s) during transaction, preventing writes from elsewhere to_start = select( - generation for generation in Generation if generation.state == STATE_QUEUED) + generation for generation in Generation + if generation.state == STATE_QUEUED).for_update() for generation in to_start: launch_generator(generator_pool, generation) except AlreadyRunningException: diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index c33d2648a70..15067e131bd 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -4,7 +4,7 @@ import random import json import zipfile from collections import Counter -from typing import Dict, Optional as TypeOptional +from typing import Dict, Optional, Any from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template @@ -15,7 +15,7 @@ from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings import pickle -from .models import * +from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID from WebHostLib import app from .check import get_yaml_data, roll_options from .upload import upload_zip_to_db @@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict: } plando_options -= {""} - meta = { + server_options = { "hint_cost": int(options_source.get("hint_cost", 10)), "forfeit_mode": options_source.get("forfeit_mode", "goal"), "remaining_mode": options_source.get("remaining_mode", "disabled"), "collect_mode": options_source.get("collect_mode", "disabled"), "item_cheat": bool(int(options_source.get("item_cheat", 1))), "server_password": options_source.get("server_password", None), - "plando_options": list(plando_options) } - return meta + return {"server_options": server_options, "plando_options": list(plando_options)} @app.route('/generate', methods=['GET', 'POST']) @@ -60,13 +59,13 @@ def generate(race=False): results, gen_options = roll_options(options, meta["plando_options"]) if race: - meta["item_cheat"] = False - meta["remaining_mode"] = "disabled" + meta["server_options"]["item_cheat"] = False + meta["server_options"]["remaining_mode"] = "disabled" if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) elif len(gen_options) > app.config["MAX_ROLL"]: - flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. " + flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( @@ -92,23 +91,22 @@ def generate(race=False): return render_template("generate.html", race=race, version=__version__) -def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None): +def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None): if not meta: - meta: Dict[str, object] = {} + meta: Dict[str, Any] = {} + + meta.setdefault("server_options", {}).setdefault("hint_cost", 10) + race = meta.setdefault("race", False) - meta.setdefault("hint_cost", 10) - race = meta.get("race", False) - del (meta["race"]) - plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"}) - del (meta["plando_options"]) try: target = tempfile.TemporaryDirectory() playercount = len(gen_options) seed = get_seed() - random.seed(seed) if race: - random.seed() # reset to time-based random source + random.seed() # use time-based random source + else: + random.seed(seed) seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)) @@ -120,7 +118,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.plando_options = PlandoSettings.from_set(plando_options) + erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options", + {"bosses", "items", "connections", "texts"})) name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): @@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.name[player] = handle_name(erargs.name[player], player, name_counter) if len(set(erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}") - ERmain(erargs, seed, baked_server_options=meta) + ERmain(erargs, seed, baked_server_options=meta["server_options"]) return upload_to_db(target.name, sid, owner, race) except BaseException as e: @@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No meta = json.loads(gen.meta) meta["error"] = (e.__class__.__name__ + ": " + str(e)) gen.meta = json.dumps(meta) - commit() raise diff --git a/inno_setup.iss b/inno_setup.iss index 1dee01af188..1005cadad0f 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -129,6 +129,7 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" [Registry] diff --git a/playerSettings.yaml b/playerSettings.yaml index ff3596a77a4..4ebae9e6d71 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -175,12 +175,15 @@ A Link to the Past: retro_caves: on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion. off: 50 - hints: # Vendors: King Zora and Bottle Merchant say what they're selling. - # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. + hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. 'on': 50 - vendors: 0 'off': 0 full: 0 + scams: # If on, these Merchants will no longer tell you what they're selling. + 'off': 50 + 'king_zora': 0 + 'bottle_merchant': 0 + 'all': 0 swordless: on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change off: 1 @@ -273,6 +276,7 @@ A Link to the Past: p: 0 # Randomize the prices of the items in shop inventories u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too + P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees ip: 0 # Shuffle inventories and randomize prices fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 24dda8e24f8..301c502a7e8 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1088,10 +1088,10 @@ def get_pool_core(world): placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart' skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)']) else: - placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)' + placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)' if world.shuffle_gerudo_card and world.gerudo_fortress != 'open': pool.append('Gerudo Membership Card') diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 54395714ba9..d029e58473f 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -619,6 +619,7 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items def WriteSMKeyCardDoors(self): if (not self.myWorld.Config.Keysanity): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7f05e0dfd52..e440eab2c9a 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -27,14 +27,18 @@ class SMZ3CollectionState(metaclass=AutoLogicRegister): # for unit tests where MultiWorld is instantiated before worlds if hasattr(parent, "state"): self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")} + for player, group in parent.groups.items(): + if (group["game"] == "SMZ3"): + self.smz3state[player] = TotalSMZ3Item.Progression([]) + if player not in parent.state.smz3state: + parent.state.smz3state[player] = TotalSMZ3Item.Progression([]) else: self.smz3state = {} def copy_mixin(self, ret) -> CollectionState: - ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.world.get_game_players("SMZ3")} + ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state} return ret - class SMZ3Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -106,6 +110,7 @@ class SMZ3World(World): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) allJunkItems = niceItems + junkItems + self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): progressionItems = self.progression + self.dungeon + self.keyCardsItems @@ -256,11 +261,11 @@ class SMZ3World(World): base_combined_rom = basepatch.apply(base_combined_rom) patcher = TotalSMZ3Patch(self.smz3World, - [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World)], + [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.world.seed_name, self.world.seed, self.local_random, - self.world.world_name_lookup, + {v: k for k, v in self.world.player_name.items()}, next(iter(loc.player for loc in self.world.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player)))) patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) @@ -312,7 +317,7 @@ class SMZ3World(World): return slot_data def collect(self, state: CollectionState, item: Item) -> bool: - state.smz3state[item.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: state.prog_items[item.name, item.player] += 1 return True # indicate that a logical state change has occured @@ -321,7 +326,7 @@ class SMZ3World(World): def remove(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item, True) if name: - state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) state.prog_items[name, item.player] -= 1 if state.prog_items[name, item.player] < 1: del (state.prog_items[name, item.player]) @@ -330,7 +335,9 @@ class SMZ3World(World): def create_item(self, name: str) -> Item: return SMZ3Item(name, ItemClassification.progression, - TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], player = self.player) + TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], + self.player, + TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) def pre_fill(self): from Fill import fill_restrictive @@ -364,6 +371,9 @@ class SMZ3World(World): else: return [] + def get_filler_item_name(self) -> str: + return self.world.random.choice(self.junkItemsNames) + def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index faf7443a573..6faeeaa2fb4 100644 Binary files a/worlds/smz3/data/zsm.ips and b/worlds/smz3/data/zsm.ips differ diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py new file mode 100644 index 00000000000..56e2a7efa1d --- /dev/null +++ b/worlds/subnautica/Creatures.py @@ -0,0 +1,82 @@ +from typing import Dict, Set, List + +# EN Locale Creature Name to rough depth in meters found at +all_creatures: Dict[str, int] = { + "Gasopod": 0, + "Bladderfish": 0, + "Ancient Floater": 0, + "Skyray": 0, + "Garryfish": 0, + "Peeper": 0, + "Shuttlebug": 0, + "Rabbit Ray": 0, + "Stalker": 0, + "Floater": 0, + "Holefish": 0, + "Cave Crawler": 0, + "Hoopfish": 0, + "Crashfish": 0, + "Hoverfish": 0, + "Spadefish": 0, + "Reefback Leviathan": 0, + "Reaper Leviathan": 0, + "Warper": 0, + "Boomerang": 0, + "Biter": 200, + "Sand Shark": 200, + "Bleeder": 200, + "Crabsnake": 300, + "Jellyray": 300, + "Oculus": 300, + "Mesmer": 300, + "Eyeye": 300, + "Reginald": 400, + "Sea Treader Leviathan": 400, + "Crabsquid": 400, + "Ampeel": 400, + "Boneshark": 400, + "Rockgrub": 400, + "Ghost Leviathan": 500, + "Ghost Leviathan Juvenile": 500, + "Spinefish": 600, + "Blighter": 600, + "Blood Crawler": 600, + "Ghostray": 1000, + "Amoeboid": 1000, + "River Prowler": 1000, + "Red Eyeye": 1300, + "Magmarang": 1300, + "Crimson Ray": 1300, + "Lava Larva": 1300, + "Lava Lizard": 1300, + "Sea Dragon Leviathan": 1300, + "Sea Emperor Leviathan": 1700, + "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 +aggressive: Set[str] = { + "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency + "Crashfish", + "Bleeder", + "Mesmer", + "Reaper Leviathan", + "Crabsquid", + "Warper", + "Crabsnake", + "Ampeel", + "Boneshark", + "Lava Lizard", + "Sea Dragon Leviathan", + "River Prowler", +} + +suffix: str = " Scan" + +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) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index b55efe24535..f3a6ded5aac 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -166,7 +166,7 @@ item_table: Dict[int, ItemDict] = { 'count': 5, 'name': 'Seamoth Fragment', 'tech_type': 'SeamothFragment'}, - 35039: {'classification': ItemClassification.useful, + 35039: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Stasis Rifle Fragment', 'tech_type': 'StasisRifleFragment'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index cae7ba6c0e4..b5dc2241fb0 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,5 @@ -from Options import Choice +from Options import Choice, Range +from .Creatures import all_creatures class ItemPool(Choice): @@ -31,7 +32,15 @@ class Goal(Choice): }[self.value] +class CreatureScans(Range): + """Place items on specific creature scans. + Warning: Includes aggressive Leviathans.""" + display_name = "Creature Scans" + range_end = len(all_creatures) + + options = { "item_pool": ItemPool, "goal": Goal, + "creature_scans": CreatureScans } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 131a537f047..b8f8f1a7b48 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,112 +1,122 @@ +from typing import TYPE_CHECKING + from worlds.generic.Rules import set_rule from .Locations import location_table, LocationDict +from .Creatures import all_creatures, aggressive, suffix import math +if TYPE_CHECKING: + from . import SubnauticaWorld -def has_seaglide(state, player): + +def has_seaglide(state, player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player): +def has_modification_station(state, player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player): +def has_mobile_vehicle_bay(state, player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player): +def has_moonpool(state, player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player): +def has_vehicle_upgrade_console(state, player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player): +def has_seamoth(state, player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player): +def has_seamoth_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player): +def has_seamoth_depth_module_mk2(state, player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player): +def has_seamoth_depth_module_mk3(state, player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player): +def has_cyclops_bridge(state, player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player): +def has_cyclops_engine(state, player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player): +def has_cyclops_hull(state, player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player): +def has_cyclops(state, 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): +def has_cyclops_depth_module_mk1(state, player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player): +def has_cyclops_depth_module_mk2(state, player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player): +def has_cyclops_depth_module_mk3(state, player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player): +def has_prawn(state, player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player): +def has_praw_propulsion_arm(state, 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): +def has_prawn_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player): +def has_prawn_depth_module_mk2(state, player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player): +def has_laser_cutter(state, player: int): return state.has("Laser Cutter Fragment", player, 3) +def has_stasis_rile(state, player: int): + return state.has("Stasis Rifle Fragment", player, 2) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player): +def has_propulsion_cannon(state, player: int): 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): +def has_cyclops_shield(state, player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -119,7 +129,7 @@ def has_cyclops_shield(state, player): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player): +def get_max_swim_depth(state, player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -130,7 +140,7 @@ def get_max_swim_depth(state, 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_seaglide(state, player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -146,7 +156,7 @@ def get_max_swim_depth(state, player): # return max_depth -def get_seamoth_max_depth(state, player): +def get_seamoth_max_depth(state, player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -186,7 +196,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player): +def get_max_depth(state, 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 @@ -196,7 +206,7 @@ def get_max_depth(state, player): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict): +def can_access_location(state, 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 @@ -225,17 +235,33 @@ def can_access_location(state, player: int, loc: LocationDict): return get_max_depth(state, player) >= depth -def set_location_rule(world, player, loc): +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 set_rules(subnautica_world): +def can_scan_creature(state, 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), + lambda state: can_scan_creature(state, player, creature_name)) + + +def set_rules(subnautica_world: "SubnauticaWorld"): player = subnautica_world.player world = subnautica_world.world 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) + # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: get_max_depth(state, player) >= 1444 and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index f2fa5497cf0..9ad4feb1a40 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -5,6 +5,7 @@ from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassifi from worlds.AutoWorld import World, WebWorld from . import Items from . import Locations +from . import Creatures from . import Options from .Items import item_table from .Rules import set_rules @@ -23,6 +24,10 @@ class SubnaticaWeb(WebWorld): )] +all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} +all_locations.update(Creatures.creature_locations) + + class SubnauticaWorld(World): """ Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by @@ -33,25 +38,30 @@ class SubnauticaWorld(World): web = SubnaticaWeb() item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} - location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_to_id = all_locations options = Options.options - data_version = 2 + data_version = 3 required_client_version = (0, 3, 3) prefill_items: List[Item] + creatures_to_scan: List[str] def generate_early(self) -> None: self.prefill_items = [ self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] + self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + self.world.creature_scans[self.player].value) def create_regions(self): self.world.regions += [ self.create_region("Menu", None, ["Lifepod 5"]), self.create_region("Planet 4546B", - Locations.events + [location["name"] for location in Locations.location_table.values()]) + Locations.events + + [location["name"] for location in Locations.location_table.values()] + + [creature+Creatures.suffix for creature in self.creatures_to_scan]) ] # refer to Rules.py @@ -64,7 +74,7 @@ class SubnauticaWorld(World): # Generate item pool pool = [] neptune_launch_platform = None - extras = 0 + extras = self.world.creature_scans[self.player].value valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable for item in item_table.values(): for i in range(item["count"]): @@ -105,6 +115,7 @@ class SubnauticaWorld(World): slot_data: Dict[str, Any] = { "goal": goal.current_key, "vanilla_tech": vanilla_tech, + "creatures_to_scan": self.creatures_to_scan } return slot_data