From 4b18920819a71a44b2a6455a2632ea8fb843028d Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 27 Oct 2022 03:00:24 -0400 Subject: [PATCH] Early Items option (#1086) * Early Items option * Early Items description update * Change Early Items to dict * Rewrite early items as extra fill steps * Move if statement up * Document early_items * Move early_items handling before fill_hook * Apply suggestions from code review Co-authored-by: Doug Hoskisson * Subnautica pre-fill moved to early_items * Subnautica early items fix * Remove unit test bug workaround * beauxq's pr Co-authored-by: Doug Hoskisson --- BaseClasses.py | 1 + Fill.py | 39 +++++++++++++++++++++ Options.py | 6 ++++ worlds/generic/docs/advanced_settings_en.md | 9 ++++- worlds/subnautica/Items.py | 2 +- worlds/subnautica/__init__.py | 19 ++-------- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ce2fc9e3c5..016c80ec83 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 cb9844b442..105b359134 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 536f388efb..ad87f5ebf8 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 d19c9d5ee6..45f653e8bb 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 4201cf3910..4a9eeabdfd 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 830bc831ef..bd86dc5ce7 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"