LADX: tarins gift improvement (#3970)

* add groups and a preset

* formatting

* pull zig's tarin's gift improvements

* typing

* alias groups for progressive items

* change tarins gift option a bit

* add bush breakers item group

* fix typo

* bush_breaker option, respect non_local_items

* review suggestions

* cleaner
thx exempt

* Update worlds/ladx/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* fix gen failures for dungeon shuffle

* exclude shovel based on entrance mapping

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
threeandthreee
2025-03-07 19:24:58 -05:00
committed by GitHub
parent bb9a6bcd2e
commit 2f0b81e12c
3 changed files with 77 additions and 38 deletions

View File

@@ -7,23 +7,12 @@ from ..roomEditor import RoomEditor
class StartItem(DroppedKey): 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 MULTIWORLD = False
def __init__(self): def __init__(self):
super().__init__(0x2A3) super().__init__(0x2A3)
self.give_bowwow = False 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): def patch(self, rom, option, *, multiworld=None):
assert multiworld is None assert multiworld is None

View File

@@ -527,6 +527,20 @@ class InGameHints(DefaultOnToggle):
display_name = "In-game Hints" 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): 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. 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", [ OptionGroup("Miscellaneous", [
TradeQuest, TradeQuest,
Rooster, Rooster,
TarinsGift,
Overworld, Overworld,
TrendyGame, TrendyGame,
InGameHints, InGameHints,
@@ -638,6 +653,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
text_mode: TextMode text_mode: TextMode
no_flash: NoFlash no_flash: NoFlash
in_game_hints: InGameHints in_game_hints: InGameHints
tarins_gift: TarinsGift
overworld: Overworld overworld: Overworld
stabilize_item_pool: StabilizeItemPool stabilize_item_pool: StabilizeItemPool

View File

@@ -4,6 +4,7 @@ import os
import pkgutil import pkgutil
import tempfile import tempfile
import typing import typing
import logging
import re import re
import bsdiff4 import bsdiff4
@@ -178,10 +179,10 @@ class LinksAwakeningWorld(World):
assert(start) 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 = [Entrance(self.player, "Start Game", menu_region)]
menu_region.exits[0].connect(start) menu_region.exits[0].connect(start)
self.multiworld.regions.append(menu_region) self.multiworld.regions.append(menu_region)
# Place RAFT, other access events # Place RAFT, other access events
@@ -189,14 +190,14 @@ class LinksAwakeningWorld(World):
for loc in region.locations: for loc in region.locations:
if loc.address is None: if loc.address is None:
loc.place_locked_item(self.create_event(loc.ladxr_item.event)) loc.place_locked_item(self.create_event(loc.ladxr_item.event))
# Connect Windfish -> Victory # Connect Windfish -> Victory
windfish = self.multiworld.get_region("Windfish", self.player) windfish = self.multiworld.get_region("Windfish", self.player)
l = Location(self.player, "Windfish", parent=windfish) l = Location(self.player, "Windfish", parent=windfish)
windfish.locations = [l] windfish.locations = [l]
l.place_locked_item(self.create_event("An Alarm Clock")) 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) self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
def create_item(self, item_name: str): def create_item(self, item_name: str):
@@ -206,6 +207,8 @@ class LinksAwakeningWorld(World):
return Item(event, ItemClassification.progression, None, self.player) return Item(event, ItemClassification.progression, None, self.player)
def create_items(self) -> None: def create_items(self) -> None:
itempool = []
exclude = [item.name for item in self.multiworld.precollected_items[self.player]] exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
@@ -265,9 +268,9 @@ class LinksAwakeningWorld(World):
self.prefill_own_dungeons.append(item) self.prefill_own_dungeons.append(item)
self.pre_fill_items.append(item) self.pre_fill_items.append(item)
else: else:
self.multiworld.itempool.append(item) itempool.append(item)
else: else:
self.multiworld.itempool.append(item) itempool.append(item)
self.multi_key = self.generate_multi_key() 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) event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
trendy_region.locations.insert(0, event_location) trendy_region.locations.insert(0, event_location)
event_location.place_locked_item(self.create_event("Can Play Trendy Game")) 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): for r in self.multiworld.get_regions(self.player):
# Set aside dungeon locations # Set aside dungeon locations
if r.dungeon_index: if r.dungeon_index:
@@ -290,21 +293,52 @@ class LinksAwakeningWorld(World):
# Properly fill locations within dungeon # Properly fill locations within dungeon
location.dungeon = r.dungeon_index location.dungeon = r.dungeon_index
# For now, special case first item if self.options.tarins_gift != "any_item":
FORCE_START_ITEM = True self.force_start_item(itempool)
if FORCE_START_ITEM:
self.force_start_item()
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) start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
if not start_loc.item: if not start_loc.item:
possible_start_items = [index for index, item in enumerate(self.multiworld.itempool) """
if item.player == self.player Find an item that forces progression or a bush breaker for the player, depending on settings.
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location] """
if possible_start_items: def is_possible_start_item(item):
index = self.random.choice(possible_start_items) return item.advancement and item.name not in self.options.non_local_items
start_item = self.multiworld.itempool.pop(index)
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) 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): def get_pre_fill_items(self):
return self.pre_fill_items return self.pre_fill_items
@@ -317,7 +351,7 @@ class LinksAwakeningWorld(World):
# set containing the list of all possible dungeon locations for the player # set containing the list of all possible dungeon locations for the player
all_dungeon_locs = set() all_dungeon_locs = set()
# Do dungeon specific things # Do dungeon specific things
for dungeon_index in range(0, 9): for dungeon_index in range(0, 9):
# set up allow-list for dungeon specific items # set up allow-list for dungeon specific items
@@ -330,7 +364,7 @@ class LinksAwakeningWorld(World):
# ...also set the rules for the dungeon # ...also set the rules for the dungeon
for location in locs: for location in locs:
orig_rule = location.item_rule 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) # 1. it fits the general rules for that location (probably 'return True' for most places)
# 2. Either # 2. Either
# 2a. it's not a restricted dungeon item # 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. # Sweep to pick up already placed items that are reachable with everything but the dungeon items.
partial_all_state.sweep_for_advancements() 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) 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: for name in possibles:
if name in self.name_cache: if name in self.name_cache:
return self.name_cache[name] return self.name_cache[name]
return "TRADING_ITEM_LETTER" return "TRADING_ITEM_LETTER"
@classmethod @classmethod
@@ -436,7 +470,7 @@ class LinksAwakeningWorld(World):
for loc in r.locations: for loc in r.locations:
if isinstance(loc, LinksAwakeningLocation): if isinstance(loc, LinksAwakeningLocation):
assert(loc.item) assert(loc.item)
# If we're a links awakening item, just use the item # If we're a links awakening item, just use the item
if isinstance(loc.item, LinksAwakeningItem): if isinstance(loc.item, LinksAwakeningItem):
loc.ladxr_item.item = loc.item.item_data.ladxr_id 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"]) args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
rom = generator.generateRom(args, self) rom = generator.generateRom(args, self)
with open(out_path, "wb") as handle: with open(out_path, "wb") as handle:
rom.save(handle, name="LADXR") rom.save(handle, name="LADXR")
@@ -478,7 +512,7 @@ class LinksAwakeningWorld(World):
if self.options.ap_title_screen: if self.options.ap_title_screen:
with tempfile.NamedTemporaryFile(delete=False) as title_patch: with tempfile.NamedTemporaryFile(delete=False) as title_patch:
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
bsdiff4.file_patch_inplace(out_path, title_patch.name) bsdiff4.file_patch_inplace(out_path, title_patch.name)
os.unlink(title_patch.name) os.unlink(title_patch.name)