diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 8ad889fd3b..38a3e47809 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -9,7 +9,7 @@ from .items import item_table, item_names, copy_ability_table, animal_friend_tab trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\ lookup_item_to_id from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations -from .names.animal_friend_spawns import animal_friend_spawns +from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive from .regions import create_levels, default_levels from .options import KDL3Options, kdl3_option_groups @@ -213,6 +213,24 @@ class KDL3World(World): self.collect(allstate, self.create_item(item)) self.random.shuffle(locations) fill_restrictive(self.multiworld, allstate, locations, items, True, True) + + # Need to ensure all of these are unique items, and replace them if they aren't + for spawns in problematic_sets: + placed = [self.get_location(spawn).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + if len(placed_names) != len(placed): + # have a duplicate + animals = [] + for spawn in spawns: + spawn_location = self.get_location(spawn) + if spawn_location.item.name not in animals: + animals.append(spawn_location.item.name) + else: + new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn", + "ChuChu Spawn", "Nago Spawn", "Pitch Spawn"] + if x not in placed_names]) + spawn_location.item = self.create_item(new_animal) + # logically, this should be sound pre-ER. May need to adjust around it with ER in the future else: animal_friends = animal_friend_spawns.copy() for animal in animal_friends: diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 index d16fef7b7f..369e91ec3c 100644 Binary files a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 and b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 differ diff --git a/worlds/kdl3/names/animal_friend_spawns.py b/worlds/kdl3/names/animal_friend_spawns.py index 4520cf1438..5c1ba39697 100644 --- a/worlds/kdl3/names/animal_friend_spawns.py +++ b/worlds/kdl3/names/animal_friend_spawns.py @@ -1,3 +1,5 @@ +from typing import List + grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu @@ -197,3 +199,12 @@ animal_friend_spawns = { iceberg_6_a5: "ChuChu Spawn", iceberg_6_a6: "Nago Spawn", } + +problematic_sets: List[List[str]] = [ + # Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not. + [ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3], + [sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3], + [cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3], + [iceberg_6_a1, iceberg_6_a2, iceberg_6_a3], + [iceberg_6_a4, iceberg_6_a5, iceberg_6_a6] +] diff --git a/worlds/kdl3/rules.py b/worlds/kdl3/rules.py index b7a141b718..a08e99257e 100644 --- a/worlds/kdl3/rules.py +++ b/worlds/kdl3/rules.py @@ -1,5 +1,5 @@ from worlds.generic.Rules import set_rule, add_rule -from .names import location_name, enemy_abilities +from .names import location_name, enemy_abilities, animal_friend_spawns from .locations import location_table from .options import GoalSpeed import typing @@ -302,6 +302,13 @@ def set_rules(world: "KDL3World") -> None: set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) + # animal friend rules + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player), + lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), + lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) + and can_reach_burning(state, world.player)) + for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 5 Boss - Purified"], diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm index dc0c719398..2ba56f784a 100644 --- a/worlds/kdl3/src/kdl3_basepatch.asm +++ b/worlds/kdl3/src/kdl3_basepatch.asm @@ -240,6 +240,9 @@ MainLoopHook: BEQ .Return ; return if we are LDA $5541 ; gooey status BPL .Slowness ; gooey is already spawned + LDA $39D1 ; is kirby alive? + BEQ .Slowness ; branch if he isn't + ; maybe BMI here too? LDA $8080 CMP #$0000 ; did we get a gooey trap BEQ .Slowness ; branch if we did not @@ -432,17 +435,47 @@ AnimalFriendSpawn: BNE .Return XBA PHA + PHX + PHA + LDX #$0000 + .CheckSpawned: + LDA $05CA, X + BNE .Continue + LDA #$0002 + CMP $074A, X + BNE .ContinueCheck + PLA + PHA + XBA + CMP $07CA, X + BEQ .AlreadySpawned + .ContinueCheck: + INX + INX + BRA .CheckSpawned + .Continue: + PLA + PLX ASL TAY PLA INC CMP $8000, Y ; do we have this animal friend BEQ .Return ; we have this animal friend + .False: INX .Return: PLY LDA #$9999 RTL + .AlreadySpawned: + PLA + PLX + ASL + TAY + PLA + BRA .False + WriteBWRAM: LDY #$6001 ;starting addr diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py index 852f35dc5c..ffa70977f7 100644 --- a/worlds/kdl3/test/test_shuffles.py +++ b/worlds/kdl3/test/test_shuffles.py @@ -1,6 +1,7 @@ from typing import List, Tuple, Optional from . import KDL3TestBase from ..room import KDL3Room +from ..names import animal_friend_spawns class TestCopyAbilityShuffle(KDL3TestBase): @@ -174,6 +175,14 @@ class TestAnimalShuffle(KDL3TestBase): self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}") + class TestAllShuffle(KDL3TestBase): options = { @@ -238,6 +247,14 @@ class TestAllShuffle(KDL3TestBase): self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}") + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities