diff --git a/worlds/ladx/LADXR/locations/startItem.py b/worlds/ladx/LADXR/locations/startItem.py index 0421c1d6..6cd96516 100644 --- a/worlds/ladx/LADXR/locations/startItem.py +++ b/worlds/ladx/LADXR/locations/startItem.py @@ -7,23 +7,12 @@ from ..roomEditor import RoomEditor class StartItem(DroppedKey): - # We need to give something here that we can use to progress. - # FEATHER - OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB] MULTIWORLD = False def __init__(self): super().__init__(0x2A3) self.give_bowwow = False - def configure(self, options): - if options.bowwow != 'normal': - # When we have bowwow mode, we pretend to be a sword for logic reasons - self.OPTIONS = [SWORD] - self.give_bowwow = True - if options.randomstartlocation and options.entranceshuffle != 'none': - self.OPTIONS.append(FLIPPERS) - def patch(self, rom, option, *, multiworld=None): assert multiworld is None diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index a35bb870..7ea7df36 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -527,6 +527,20 @@ class InGameHints(DefaultOnToggle): display_name = "In-game Hints" +class TarinsGift(Choice): + """ + [Local Progression] Forces Tarin's gift to be an item that immediately opens up local checks. + Has little effect in single player games, and isn't always necessary with randomized entrances. + [Bush Breaker] Forces Tarin's gift to be an item that can destroy bushes. + [Any Item] Tarin's gift can be any item for any world + """ + display_name = "Tarin's Gift" + option_local_progression = 0 + option_bush_breaker = 1 + option_any_item = 2 + default = option_local_progression + + class StabilizeItemPool(DefaultOffToggle): """ By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando. @@ -565,6 +579,7 @@ ladx_option_groups = [ OptionGroup("Miscellaneous", [ TradeQuest, Rooster, + TarinsGift, Overworld, TrendyGame, InGameHints, @@ -638,6 +653,7 @@ class LinksAwakeningOptions(PerGameCommonOptions): text_mode: TextMode no_flash: NoFlash in_game_hints: InGameHints + tarins_gift: TarinsGift overworld: Overworld stabilize_item_pool: StabilizeItemPool diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index a887638e..4c22693c 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -4,6 +4,7 @@ import os import pkgutil import tempfile import typing +import logging import re import bsdiff4 @@ -178,10 +179,10 @@ class LinksAwakeningWorld(World): assert(start) - menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) + menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld) menu_region.exits = [Entrance(self.player, "Start Game", menu_region)] menu_region.exits[0].connect(start) - + self.multiworld.regions.append(menu_region) # Place RAFT, other access events @@ -189,14 +190,14 @@ class LinksAwakeningWorld(World): for loc in region.locations: if loc.address is None: loc.place_locked_item(self.create_event(loc.ladxr_item.event)) - + # Connect Windfish -> Victory windfish = self.multiworld.get_region("Windfish", self.player) l = Location(self.player, "Windfish", parent=windfish) windfish.locations = [l] - + l.place_locked_item(self.create_event("An Alarm Clock")) - + self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player) def create_item(self, item_name: str): @@ -206,6 +207,8 @@ class LinksAwakeningWorld(World): return Item(event, ItemClassification.progression, None, self.player) def create_items(self) -> None: + itempool = [] + exclude = [item.name for item in self.multiworld.precollected_items[self.player]] self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] @@ -265,9 +268,9 @@ class LinksAwakeningWorld(World): self.prefill_own_dungeons.append(item) self.pre_fill_items.append(item) else: - self.multiworld.itempool.append(item) + itempool.append(item) else: - self.multiworld.itempool.append(item) + itempool.append(item) self.multi_key = self.generate_multi_key() @@ -276,8 +279,8 @@ class LinksAwakeningWorld(World): event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) trendy_region.locations.insert(0, event_location) event_location.place_locked_item(self.create_event("Can Play Trendy Game")) - - self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] + + self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] for r in self.multiworld.get_regions(self.player): # Set aside dungeon locations if r.dungeon_index: @@ -290,21 +293,52 @@ class LinksAwakeningWorld(World): # Properly fill locations within dungeon location.dungeon = r.dungeon_index - # For now, special case first item - FORCE_START_ITEM = True - if FORCE_START_ITEM: - self.force_start_item() + if self.options.tarins_gift != "any_item": + self.force_start_item(itempool) - def force_start_item(self): + + self.multiworld.itempool += itempool + + def force_start_item(self, itempool): start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player) if not start_loc.item: - possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) - if item.player == self.player - and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location] - if possible_start_items: - index = self.random.choice(possible_start_items) - start_item = self.multiworld.itempool.pop(index) + """ + Find an item that forces progression or a bush breaker for the player, depending on settings. + """ + def is_possible_start_item(item): + return item.advancement and item.name not in self.options.non_local_items + + def opens_new_regions(item): + collection_state = base_collection_state.copy() + collection_state.collect(item) + return len(collection_state.reachable_regions[self.player]) > reachable_count + + start_items = [item for item in itempool if is_possible_start_item(item)] + self.random.shuffle(start_items) + + if self.options.tarins_gift == "bush_breaker": + start_item = next((item for item in start_items if item.name in links_awakening_item_name_groups["Bush Breakers"]), None) + + else: # local_progression + entrance_mapping = self.ladxr_logic.world_setup.entrance_mapping + # Tail key opens a region but not a location if d1 entrance is not mapped to d1 or d4 + # exclude it in these cases to avoid fill errors + if entrance_mapping['d1'] not in ['d1', 'd4']: + start_items = [item for item in start_items if item.name != 'Tail Key'] + # Exclude shovel unless starting in Mabe Village + if entrance_mapping['start_house'] not in ['start_house', 'shop']: + start_items = [item for item in start_items if item.name != 'Shovel'] + base_collection_state = CollectionState(self.multiworld) + base_collection_state.update_reachable_regions(self.player) + reachable_count = len(base_collection_state.reachable_regions[self.player]) + start_item = next((item for item in start_items if opens_new_regions(item)), None) + + if start_item: + itempool.remove(start_item) start_loc.place_locked_item(start_item) + else: + logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.") + def get_pre_fill_items(self): return self.pre_fill_items @@ -317,7 +351,7 @@ class LinksAwakeningWorld(World): # set containing the list of all possible dungeon locations for the player all_dungeon_locs = set() - + # Do dungeon specific things for dungeon_index in range(0, 9): # set up allow-list for dungeon specific items @@ -330,7 +364,7 @@ class LinksAwakeningWorld(World): # ...also set the rules for the dungeon for location in locs: orig_rule = location.item_rule - # If an item is about to be placed on a dungeon location, it can go there iff + # If an item is about to be placed on a dungeon location, it can go there iff # 1. it fits the general rules for that location (probably 'return True' for most places) # 2. Either # 2a. it's not a restricted dungeon item @@ -382,7 +416,7 @@ class LinksAwakeningWorld(World): # Sweep to pick up already placed items that are reachable with everything but the dungeon items. partial_all_state.sweep_for_advancements() - + fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) @@ -421,7 +455,7 @@ class LinksAwakeningWorld(World): for name in possibles: if name in self.name_cache: return self.name_cache[name] - + return "TRADING_ITEM_LETTER" @classmethod @@ -436,7 +470,7 @@ class LinksAwakeningWorld(World): for loc in r.locations: if isinstance(loc, LinksAwakeningLocation): assert(loc.item) - + # If we're a links awakening item, just use the item if isinstance(loc.item, LinksAwakeningItem): loc.ladxr_item.item = loc.item.item_data.ladxr_id @@ -470,7 +504,7 @@ class LinksAwakeningWorld(World): args = parser.parse_args([rom_name, "-o", out_name, "--dump"]) rom = generator.generateRom(args, self) - + with open(out_path, "wb") as handle: rom.save(handle, name="LADXR") @@ -478,7 +512,7 @@ class LinksAwakeningWorld(World): if self.options.ap_title_screen: with tempfile.NamedTemporaryFile(delete=False) as title_patch: title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) - + bsdiff4.file_patch_inplace(out_path, title_patch.name) os.unlink(title_patch.name)