From 3c819ec781b696594fad558397bc4f17ef3c0dcf Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:57:47 -0600 Subject: [PATCH] LttP: logic fixes and missing bombs (#5645) 3 logic issues: * #3046 made it so that prizes were included in LttP's pre_fill items. It accounted for it in regular pre_fill, but missed stage_pre_fill. * LttP defines a maximum number of heart pieces and heart containers logically within each difficulty. Item condensing did not account for this, and could reduce the number of heart pieces below the required amount logically. Notably, this makes some combination of settings much harder to generate, so another solution may end up ideal. * Current logic rules do not properly account for the case of standard start and enemizer, requiring a large amount of items logically within a short number of locations. However, the behavior of Enemizer in this situation is well-defined, as the guards during the standard starting sequence are not changed. Thus the required items can be safely minimized. --- worlds/alttp/Dungeons.py | 2 +- worlds/alttp/ItemPool.py | 16 ++++++++++------ worlds/alttp/Rom.py | 4 ++-- worlds/alttp/Rules.py | 13 +++++++------ worlds/alttp/StateHelpers.py | 17 ++++++++++++++--- worlds/alttp/__init__.py | 4 ++++ 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 6b7da69593..04d107f1ff 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -239,7 +239,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): multiworld.worlds[item.player].collect(all_state_base, item) pre_fill_items = [] for player in in_dungeon_player_ids: - pre_fill_items += multiworld.worlds[player].get_pre_fill_items() + pre_fill_items += [item for item in multiworld.worlds[player].get_pre_fill_items() if not item.crystal] for item in in_dungeon_items: try: pre_fill_items.remove(item) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 53059c64bc..7b64e6e5d3 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -2,7 +2,7 @@ from collections import namedtuple import logging from BaseClasses import ItemClassification -from Fill import FillError +from Options import OptionError from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType @@ -410,15 +410,16 @@ def generate_itempool(world): pool_count = len(items) new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)] if world.options.shuffle_capacity_upgrades or world.options.bombless_start: - progressive = world.options.progressive - progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on' + progressive = world.options.progressive.want_progressives(world.random) if world.options.shuffle_capacity_upgrades == "on_combined": new_items.append("Bomb Upgrade (50)") elif world.options.shuffle_capacity_upgrades == "on": new_items += ["Bomb Upgrade (+5)"] * 6 new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") - if world.options.shuffle_capacity_upgrades != "on_combined" and world.options.bombless_start: - new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") + if world.options.bombless_start: + new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") + elif world.options.bombless_start: + new_items.append("Bomb Upgrade (+10)") if world.options.shuffle_capacity_upgrades and not world.options.retro_bow: if world.options.shuffle_capacity_upgrades == "on_combined": @@ -466,6 +467,9 @@ def generate_itempool(world): items_were_cut = items_were_cut or cut_item(items, *reduce_item) elif len(reduce_item) == 4: items_were_cut = items_were_cut or condense_items(items, *reduce_item) + if reduce_item[0] == "Piece of Heart" and world.logical_heart_pieces: + world.logical_heart_pieces -= reduce_item[2] + world.logical_heart_containers += reduce_item[3] elif len(reduce_item) == 1: # Bottles bottles = [item for item in items if item.name in item_name_groups["Bottles"]] if len(bottles) > 4: @@ -476,7 +480,7 @@ def generate_itempool(world): if items_were_cut: break else: - raise Exception(f"Failed to limit item pool size for player {player}") + raise OptionError(f"Failed to limit item pool size for player {player}") if len(items) < pool_count: items += removed_filler[len(items) - pool_count:] diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 6a5792d21a..2e0b81a4bd 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1197,8 +1197,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): 0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x58, 0x01, 0x36 if local_world.options.retro_bow else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) - 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 - 0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20 + 0x3E, local_world.logical_heart_containers, 0x47, 0xff, # boss heart -> green 20 + 0x17, local_world.logical_heart_pieces, 0x47, 0xff, # piece of heart -> green 20 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel ]) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index a5b14e0c2d..18e2965d8c 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -20,7 +20,7 @@ from .StateHelpers import (can_extend_magic, can_kill_most_things, has_fire_source, has_hearts, has_melee_weapon, has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, has_triforce_pieces, can_use_bombs, can_bomb_or_bonk, - can_activate_crystal_switch) + can_activate_crystal_switch, can_kill_standard_start) from .UnderworldGlitchRules import underworld_glitches_rules @@ -1093,22 +1093,23 @@ def standard_rules(world, player): if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal: set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) - and can_kill_most_things(state, player, 2)) + and can_kill_standard_start(state, player, 2)) set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) - and can_kill_most_things(state, player, 1)) - + and can_kill_standard_start(state, player, 1)) + set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), + lambda state: can_kill_standard_start(state, player, 1)) set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)) set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and state.has('Big Key (Hyrule Castle)', player) and (world.worlds[player].options.enemy_health in ("easy", "default") - or can_kill_most_things(state, player, 1))) + or can_kill_standard_start(state, player, 1))) set_rule(world.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) - and can_kill_most_things(state, player, 1)) + and can_kill_standard_start(state, player, 1)) else: set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.has('Big Key (Hyrule Castle)', player)) diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 98409c8a8d..2c1b5e2ab8 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -59,10 +59,11 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int: def heart_count(state: CollectionState, player: int) -> int: # Warning: This only considers items that are marked as advancement items - diff = state.multiworld.worlds[player].difficulty_requirements - return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \ + max_heart_pieces = state.multiworld.worlds[player].logical_heart_pieces + max_heart_containers = state.multiworld.worlds[player].logical_heart_containers + return min(state.count('Boss Heart Container', player), max_heart_containers) \ + state.count('Sanctuary Heart Container', player) \ - + min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + + min(state.count('Piece of Heart', player), max_heart_pieces) // 4 \ + 3 # starting hearts @@ -139,6 +140,16 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) and can_use_bombs(state, player, enemies * 4))) +def can_kill_standard_start(state: CollectionState, player: int, enemies: int = 5) -> bool: + # Enemizer does not randomize standard start enemies + return (has_melee_weapon(state, player) + or state.has('Cane of Somaria', player) + or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) + or state.has_any(["Bow", "Progressive Bow"], player) + or state.has('Fire Rod', player) + or can_use_bombs(state, player, enemies)) # Escape assist is set + + def can_get_good_bee(state: CollectionState, player: int) -> bool: cave = state.multiworld.get_region('Good Bee Cave', player) return ( diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 4ee5b9d266..2b99162837 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -305,6 +305,8 @@ class ALTTPWorld(World): self.required_medallions = ["Ether", "Quake"] self.escape_assist = [] self.shops = [] + self.logical_heart_containers = 10 + self.logical_heart_pieces = 24 super(ALTTPWorld, self).__init__(*args, **kwargs) @classmethod @@ -384,6 +386,8 @@ class ALTTPWorld(World): self.options.local_items.value |= self.dungeon_local_item_names self.difficulty_requirements = difficulties[self.options.item_pool.current_key] + self.logical_heart_pieces = self.difficulty_requirements.heart_piece_limit + self.logical_heart_containers = self.difficulty_requirements.boss_heart_container_limit # enforce pre-defined local items. if self.options.goal in ["local_triforce_hunt", "local_ganon_triforce_hunt"]: