diff --git a/BaseClasses.py b/BaseClasses.py index ce2fc9e3..016c80ec 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -48,6 +48,7 @@ class MultiWorld(): state: CollectionState accessibility: Dict[int, Options.Accessibility] + early_items: Dict[int, Options.EarlyItems] local_items: Dict[int, Options.LocalItems] non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] diff --git a/Fill.py b/Fill.py index cb9844b4..105b3591 100644 --- a/Fill.py +++ b/Fill.py @@ -258,6 +258,45 @@ def distribute_items_restrictive(world: MultiWorld) -> None: usefulitempool: typing.List[Item] = [] filleritempool: typing.List[Item] = [] + early_items_count: typing.Dict[typing.Tuple[str, int], int] = {} + for player in world.player_ids: + for item, count in world.early_items[player].value.items(): + early_items_count[(item, player)] = count + if early_items_count: + early_locations: typing.List[Location] = [] + early_priority_locations: typing.List[Location] = [] + for loc in reversed(fill_locations): + if loc.can_reach(world.state): + if loc.progress_type == LocationProgressType.PRIORITY: + early_priority_locations.append(loc) + else: + early_locations.append(loc) + fill_locations.remove(loc) + + early_prog_items: typing.List[Item] = [] + early_rest_items: typing.List[Item] = [] + for item in reversed(itempool): + if (item.name, item.player) in early_items_count: + if item.advancement: + early_prog_items.append(item) + else: + early_rest_items.append(item) + itempool.remove(item) + early_items_count[(item.name, item.player)] -= 1 + if early_items_count[(item.name, item.player)] == 0: + del early_items_count[(item.name, item.player)] + fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True) + early_locations += early_priority_locations + fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True) + unplaced_early_items = early_rest_items + early_prog_items + if unplaced_early_items: + logging.warning(f"Ran out of early locations for early items. Failed to place \ + {len(unplaced_early_items)} items early.") + itempool += unplaced_early_items + + fill_locations += early_locations + early_priority_locations + world.random.shuffle(fill_locations) + for item in itempool: if item.advancement: progitempool.append(item) diff --git a/Options.py b/Options.py index 536f388e..ad87f5eb 100644 --- a/Options.py +++ b/Options.py @@ -883,6 +883,11 @@ class NonLocalItems(ItemSet): display_name = "Not Local Items" +class EarlyItems(ItemDict): + """Force the specified items to be in locations that are reachable from the start.""" + display_name = "Early Items" + + class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True @@ -981,6 +986,7 @@ per_game_common_options = { **common_options, # can be overwritten per-game "local_items": LocalItems, "non_local_items": NonLocalItems, + "early_items": EarlyItems, "start_inventory": StartInventory, "start_hints": StartHints, "start_location_hints": StartLocationHints, diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index d19c9d5e..45f653e8 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -106,7 +106,7 @@ settings. If a game can be rolled it **must** have a settings section even if it Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. -Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` +Currently, these options are `start_inventory`, `early_items`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` , `exclude_locations`, and various plando options. See the plando guide for more info on plando options. Plando @@ -115,6 +115,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you 30 rupees. +* `early_items` is formatted in the same way as `start_inventory` and will force the number of each item specified to be +forced into locations that are reachable from the start, before obtaining any items. * `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for the location without using any hint points. * `local_items` will force any items you want to be in your world instead of being in another world. @@ -172,6 +174,8 @@ A Link to the Past: - Quake non_local_items: - Moon Pearl + early_items: + Flute: 1 start_location_hints: - Spike Cave priority_locations: @@ -235,6 +239,9 @@ Timespinner: * `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we have to find it ourselves. * `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it. +* `early_items` forces the `Flute` to be placed in a location that is available from the beginning of the game ("Sphere +1"). Since it is not specified in `local_items` or `non_local_items`, it can be placed one of these locations in any +world. * `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld that can be used for no cost. * `priority_locations` forces a progression item to be placed on the `Link's House` location. diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 4201cf39..4a9eeabd 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -175,7 +175,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Thermal Plant Fragment', 'tech_type': 'ThermalPlantFragment'}, 35041: {'classification': ItemClassification.progression, - 'count': 2, + 'count': 4, 'name': 'Seaglide Fragment', 'tech_type': 'SeaglideFragment'}, 35042: {'classification': ItemClassification.progression, diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 830bc831..bd86dc5c 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,14 +44,12 @@ class SubnauticaWorld(World): data_version = 7 required_client_version = (0, 3, 5) - 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") - ] + if "Seaglide Fragment" not in self.world.early_items[self.player]: + self.world.early_items[self.player].value["Seaglide Fragment"] = 2 + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] creature_pool = scan_option.get_pool() @@ -149,17 +147,6 @@ class SubnauticaWorld(World): ret.exits.append(Entrance(self.player, region_exit, ret)) return ret - def get_pre_fill_items(self) -> List[Item]: - return self.prefill_items - - def pre_fill(self) -> None: - reachable = [location for location in self.world.get_reachable_locations(player=self.player) - if not location.item] - self.world.random.shuffle(reachable) - items = self.prefill_items.copy() - for item in items: - reachable.pop().place_locked_item(item) - class SubnauticaLocation(Location): game: str = "Subnautica"