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):
# 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

View File

@@ -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

View File

@@ -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)