diff --git a/Fill.py b/Fill.py index 0dd28574..83d7a737 100644 --- a/Fill.py +++ b/Fill.py @@ -722,8 +722,13 @@ def fast_fill_pot_for_multiworld(world, item_pool, fill_locations): flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count fill_count = len(pot_fill_locations[player]) - flex if fill_count > 0: - fill_spots = random.sample(pot_fill_locations[player], fill_count) - fill_items = random.sample(pot_item_pool[player], fill_count) + first_count = min(fill_count, len(pot_item_pool[player])) + fill_items = random.sample(pot_item_pool[player], first_count) + remaining = fill_count - first_count + if remaining > 0: + other_items = [i for i in item_pool if i.player == player and i not in pot_item_pool[player]] + fill_items += random.sample(other_items, min(remaining, len(other_items))) + fill_spots = random.sample(pot_fill_locations[player], len(fill_items)) for x in fill_items: item_pool.remove(x) for x in fill_spots: diff --git a/ItemList.py b/ItemList.py index a95604c7..f70aca19 100644 --- a/ItemList.py +++ b/ItemList.py @@ -302,6 +302,42 @@ def generate_itempool(world, player): for _ in range(0, amt): pool.append('Rupees (20)') + if world.shopsanity[player] and not skip_pool_adjustments: + for shop in world.shops[player]: + if shop.region.name in shop_to_location_table: + for index, slot in enumerate(shop.inventory): + if slot: + item = slot['item'] + if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal': + pool.append('Rupees (20)') + else: + pool.append(item) + + if (world.customizer and world.customizer.get_item_pool_adjust() + and player in world.customizer.get_item_pool_adjust()): + diff = difficulties[world.difficulty[player]] + for item_name, delta in world.customizer.get_item_pool_adjust()[player].items(): + if not isinstance(delta, int): + continue + if delta > 0: + if item_name == 'Bottle (Random)': + for _ in range(delta): + pool.append(random.choice(diff.bottles)) + else: + pool.extend([item_name] * delta) + elif delta < 0: + remove_count = abs(delta) + if item_name == 'Bottle (Random)': + bottle_names = set(diff.bottles) + for _ in range(remove_count): + bottle = next((x for x in pool if x in bottle_names), None) + if bottle: + pool.remove(bottle) + else: + for _ in range(remove_count): + if item_name in pool: + pool.remove(item_name) + if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave'] \ and world.keyshuffle[player] not in ['none', 'nearby']: # In HMG force swamp smalls in pots to allow getting out of swamp palace @@ -346,17 +382,6 @@ def generate_itempool(world, player): loc.event = True loc.locked = True - if world.shopsanity[player] and not skip_pool_adjustments: - for shop in world.shops[player]: - if shop.region.name in shop_to_location_table: - for index, slot in enumerate(shop.inventory): - if slot: - item = slot['item'] - if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal': - pool.append('Rupees (20)') - else: - pool.append(item) - items = ItemFactory(pool, player) if world.shopsanity[player]: for potion in ['Green Potion', 'Blue Potion', 'Red Potion']: diff --git a/Main.py b/Main.py index fe21aeb2..a7c52f7d 100644 --- a/Main.py +++ b/Main.py @@ -41,7 +41,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.5.5' +version_number = '1.5.6' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/PastReleaseNotes.md b/PastReleaseNotes.md index e2915b60..d7984bec 100644 --- a/PastReleaseNotes.md +++ b/PastReleaseNotes.md @@ -10,6 +10,9 @@ # Patch Notes Changelog archive +* 1.5.5 + * Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress) + * HUD: Key counters are correct even when door shuffle is off * 1.5.4 * Documentation: New AI-assisted documentation [Site](https://aerinon.github.io/ALttPDoorRandomizer) * Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0fad9133..12904fb5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,8 +141,10 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes -* 1.5.5 - * Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress) - * HUD: Key counters are correct even when door shuffle is off - +* 1.5.6 + * Enemy Drops: Pikit are no longer eligible for dropped items due to a vanilla bug where a failed steal can overwrite the assigned drop. + * Standard Mode: Reworked spawn refills to be more generous for enemy drop modes. + * Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. + * Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. + diff --git a/Rom.py b/Rom.py index 5cc3b492..b5cce11a 100644 --- a/Rom.py +++ b/Rom.py @@ -1639,38 +1639,38 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bomb_max, magic_max = 0, 0, 0 - bow_small, magic_small = 0, 0 + bow_small, bomb_small, magic_small = 10, 3, 0x20 if world.mode[player] == 'standard': if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: rom.write_byte(0x18004E, 1) # Escape Fill (arrows) write_int16(rom, 0x180183, 300) # Escape fill rupee bow rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) - bow_max, bow_small = 70, 10 + rom.write_bytes(0x180188, [0, 0, 70]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 0, 70]) # Mantle respawn refills (magic, bombs, arrows) + bow_max = 70 elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0, 50, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 50, 0]) # Mantle respawn refills (magic, bombs, arrows) bomb_max = 50 elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: rom.write_byte(0x18004E, 4) # Escape Fill (magic) rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) - magic_max, magic_small = 0x80, 0x20 + rom.write_bytes(0x180188, [0x80, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0x80, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) + magic_max = 0x80 if world.doorShuffle[player] not in ['vanilla', 'basic']: # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x180188, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) # Mantle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) + rom.write_bytes(0x18018B, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest - rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)]) - rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [magic_small, 3, bow_small]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)]) + rom.write_bytes(0x180188, [magic_small, max(bomb_small, bomb_max), bow_small]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [magic_small, max(bomb_small, bomb_max), bow_small]) # Mantle respawn refills (magic, bombs, arrows) # patch swamp: Need to enable permanent drain of water as dam or swamp were moved rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) diff --git a/docs/Customizer.md b/docs/Customizer.md index 5d5e083d..7e06d739 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -68,10 +68,30 @@ Then each player can have the entire item pool defined. The name of item should `Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here. -##### Caveat - +**Note**: `item_pool` is a **full replacement** of the base item pool. Every item the player will receive must be listed. If you only want to tweak the default pool, use `item_pool_adjust` instead. + +##### Caveat + Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon. +### item_pool_adjust + +This must be defined by player. Each player number should be listed with the appropriate adjustments. + +Unlike `item_pool`, this section **adjusts** the base item pool generated by the randomizer settings rather than replacing it. Use positive values to add items and negative values to remove items. + +```yaml +item_pool_adjust: + 1: + Bottle (Random): 2 # add 2 extra random bottles + Rupees (300): 1 # add 1 extra rupee pack + Boss Heart Container: -3 # remove 3 heart containers from the base pool +``` + +`Bottle (Random)` follows the same bottle randomization rules as in `item_pool`. When removing bottles with `Bottle (Random): -N`, any bottle (regardless of contents) will be removed. + +Removals that exceed the number of that item currently in the pool are silently ignored. Item pool adjustments are applied after beemizer but before the standard-mode Link's Uncle weapon selection, so weapons added here are eligible for that placement. + ### goals This must be defined by player. Each player number should be listed with the appropriate custom goals. This section has four primary subsections for each of the current supported events: diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index c2f3943c..ced9f4d4 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -67,6 +67,14 @@ item_pool: Sanctuary Heart Container: 3 Shovel: 3 Single Arrow: 1 + Green Potion: 1 + Blue Potion: 1 + Red Potion: 1 +item_pool_adjust: + 1: + Bottle (Random): 2 # add 2 extra random bottles on top of the base pool + Magic Upgrade (1/2): 1 # add 1 extra half-magic + Boss Heart Container: -3 # remove 3 heart containers from the base pool placements: 1: Palace of Darkness - Big Chest: Hammer diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 0e66b2de..3d971f25 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -245,6 +245,11 @@ class CustomSettings(object): return self.file_source['item_pool'] return None + def get_item_pool_adjust(self): + if 'item_pool_adjust' in self.file_source: + return self.file_source['item_pool_adjust'] + return None + def get_placements(self): if 'placements' in self.file_source: return self.file_source['placements'] diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 90ee3fba..05a60de4 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -450,7 +450,7 @@ def init_enemy_stats(): EnemySprite.Stalfos: EnemyStats(EnemySprite.Stalfos, False, True, 6, health=4, dmg=1), EnemySprite.GreenZirro: EnemyStats(EnemySprite.GreenZirro, False, False, 1, health=4, dmg=5, dmask=0x80), EnemySprite.BlueZirro: EnemyStats(EnemySprite.BlueZirro, False, False, 7, health=8, dmg=3, dmask=0x80), - EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, True, 2, health=12, dmg=5), + EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, False, 2, health=12, dmg=5), EnemySprite.OldMan: EnemyStats(EnemySprite.OldMan, True, dmg=0), EnemySprite.PipeDown: EnemyStats(EnemySprite.PipeDown, True, dmg=0), diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 15b81f51..60c09f81 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -329,7 +329,7 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.Stalfos).sub_group(0, 0x1f), SpriteRequirement(EnemySprite.GreenZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.BlueZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), - SpriteRequirement(EnemySprite.Pikit).sub_group(3, 0x1b), + SpriteRequirement(EnemySprite.Pikit).no_drop().sub_group(3, 0x1b), SpriteRequirement(EnemySprite.CrystalMaiden).affix(), SpriteRequirement(EnemySprite.OldMan).affix().sub_group(2, 0x1c), SpriteRequirement(EnemySprite.PipeDown).affix(),