| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | import logging | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  | from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | from Fill import fill_restrictive | 
					
						
							|  |  |  | from Options import PerGameCommonOptions | 
					
						
							|  |  |  | from worlds.AutoWorld import World, WebWorld | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  | from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ | 
					
						
							|  |  |  |     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, 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 | 
					
						
							|  |  |  | from .presets import kdl3_options_presets | 
					
						
							|  |  |  | from .names import location_name | 
					
						
							|  |  |  | from .room import KDL3Room | 
					
						
							|  |  |  | from .rules import set_rules | 
					
						
							|  |  |  | from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH | 
					
						
							|  |  |  | from .client import KDL3SNIClient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | import os | 
					
						
							|  |  |  | import math | 
					
						
							|  |  |  | import threading | 
					
						
							|  |  |  | import base64 | 
					
						
							|  |  |  | import settings | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | logger = logging.getLogger("Kirby's Dream Land 3") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class KDL3Settings(settings.Group): | 
					
						
							|  |  |  |     class RomFile(settings.SNESRomPath): | 
					
						
							|  |  |  |         """File name of the KDL3 JP or EN rom""" | 
					
						
							|  |  |  |         description = "Kirby's Dream Land 3 ROM File" | 
					
						
							|  |  |  |         copy_to = "Kirby's Dream Land 3.sfc" | 
					
						
							|  |  |  |         md5s = [KDL3JHASH, KDL3UHASH] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     rom_file: RomFile = RomFile(RomFile.copy_to) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class KDL3WebWorld(WebWorld): | 
					
						
							|  |  |  |     theme = "partyTime" | 
					
						
							|  |  |  |     tutorials = [ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Tutorial( | 
					
						
							|  |  |  |             "Multiworld Setup Guide", | 
					
						
							|  |  |  |             "A guide to setting up the Kirby's Dream Land 3 randomizer connected to an Archipelago Multiworld.", | 
					
						
							|  |  |  |             "English", | 
					
						
							|  |  |  |             "setup_en.md", | 
					
						
							|  |  |  |             "setup/en", | 
					
						
							|  |  |  |             ["Silvris"] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  |     options_presets = kdl3_options_presets | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     option_groups = kdl3_option_groups | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class KDL3World(World): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Join Kirby and his Animal Friends on an adventure to collect Heart Stars and drive Dark Matter away from Dream Land! | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     game = "Kirby's Dream Land 3" | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     options: KDL3Options | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     item_name_to_id = lookup_item_to_id | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     location_name_to_id = {location_table[location]: location for location in location_table} | 
					
						
							|  |  |  |     item_name_groups = item_names | 
					
						
							|  |  |  |     web = KDL3WebWorld() | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     settings: ClassVar[KDL3Settings] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, multiworld: MultiWorld, player: int): | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         self.rom_name: bytes = bytes() | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         self.rom_name_available_event = threading.Event() | 
					
						
							|  |  |  |         super().__init__(multiworld, player) | 
					
						
							|  |  |  |         self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() | 
					
						
							|  |  |  |         self.required_heart_stars: int = 0  # we fill this during create_items | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         self.boss_requirements: List[int] = [] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         self.player_levels = default_levels.copy() | 
					
						
							|  |  |  |         self.stage_shuffle_enabled = False | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         self.boss_butch_bosses: List[Optional[bool]] = [] | 
					
						
							|  |  |  |         self.rooms: List[KDL3Room] = [] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     create_regions = create_levels | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def generate_early(self) -> None: | 
					
						
							|  |  |  |         if self.options.total_heart_stars != -1: | 
					
						
							|  |  |  |             logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. " | 
					
						
							|  |  |  |                            f"Please use \"max_heart_stars\" instead.") | 
					
						
							|  |  |  |             self.options.max_heart_stars.value = self.options.total_heart_stars.value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         item = item_table[name] | 
					
						
							|  |  |  |         classification = ItemClassification.filler | 
					
						
							|  |  |  |         if item.progression and not force_non_progression: | 
					
						
							|  |  |  |             classification = ItemClassification.progression_skip_balancing \ | 
					
						
							|  |  |  |                 if item.skip_balancing else ItemClassification.progression | 
					
						
							|  |  |  |         elif item.trap: | 
					
						
							|  |  |  |             classification = ItemClassification.trap | 
					
						
							|  |  |  |         return KDL3Item(name, classification, item.code, self.player) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def get_filler_item_name(self, include_stars: bool = True) -> str: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         if include_stars: | 
					
						
							|  |  |  |             return self.random.choices(list(total_filler_weights.keys()), | 
					
						
							|  |  |  |                                        weights=list(total_filler_weights.values()))[0] | 
					
						
							|  |  |  |         return self.random.choices(list(filler_item_weights.keys()), | 
					
						
							|  |  |  |                                    weights=list(filler_item_weights.values()))[0] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_trap_item_name(self) -> str: | 
					
						
							|  |  |  |         return self.random.choices(["Gooey Bag", "Slowness", "Eject Ability"], | 
					
						
							|  |  |  |                                    weights=[self.options.gooey_trap_weight.value, | 
					
						
							|  |  |  |                                             self.options.slow_trap_weight.value, | 
					
						
							|  |  |  |                                             self.options.ability_trap_weight.value])[0] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str], | 
					
						
							|  |  |  |                                                level: int, stage: int) -> Optional[str]: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         valid_rooms = [room for room in self.rooms if (room.level < level) | 
					
						
							|  |  |  |                        or (room.level == level and room.stage < stage)]  # leave out the stage in question to avoid edge | 
					
						
							|  |  |  |         valid_enemies = set() | 
					
						
							|  |  |  |         for room in valid_rooms: | 
					
						
							|  |  |  |             valid_enemies.update(room.enemies) | 
					
						
							|  |  |  |         placed_enemies = [enemy for enemy in valid_enemies if enemy not in enemies_to_set] | 
					
						
							|  |  |  |         if any(self.copy_abilities[enemy] == copy_ability for enemy in placed_enemies): | 
					
						
							|  |  |  |             return None  # a valid enemy got placed by a more restrictive placement | 
					
						
							|  |  |  |         return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies])) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def get_pre_fill_items(self) -> List[Item]: | 
					
						
							|  |  |  |         return [self.create_item(item) | 
					
						
							|  |  |  |                 for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     def pre_fill(self) -> None: | 
					
						
							|  |  |  |         if self.options.copy_ability_randomization: | 
					
						
							|  |  |  |             # randomize copy abilities | 
					
						
							|  |  |  |             valid_abilities = list(copy_ability_access_table.keys()) | 
					
						
							|  |  |  |             enemies_to_set = list(self.copy_abilities.keys()) | 
					
						
							| 
									
										
										
										
											2024-03-13 15:05:38 -05:00
										 |  |  |             unplaced_abilities = set(key for key in copy_ability_access_table.keys() | 
					
						
							|  |  |  |                                      if key not in ("No Ability", "Cutter Ability", "Burning Ability")) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             # now for the edge cases | 
					
						
							|  |  |  |             for abilities, enemies in enemy_restrictive: | 
					
						
							|  |  |  |                 available_enemies = list() | 
					
						
							|  |  |  |                 for enemy in enemies: | 
					
						
							|  |  |  |                     if enemy not in enemies_to_set: | 
					
						
							|  |  |  |                         if self.copy_abilities[enemy] in abilities: | 
					
						
							|  |  |  |                             break | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         available_enemies.append(enemy) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     chosen_enemy = self.random.choice(available_enemies) | 
					
						
							|  |  |  |                     chosen_ability = self.random.choice(abilities) | 
					
						
							|  |  |  |                     self.copy_abilities[chosen_enemy] = chosen_ability | 
					
						
							|  |  |  |                     enemies_to_set.remove(chosen_enemy) | 
					
						
							| 
									
										
										
										
											2024-03-13 15:05:38 -05:00
										 |  |  |                     unplaced_abilities.discard(chosen_ability) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             # two less restrictive ones, we need to ensure Cutter and Burning appear before their required stages | 
					
						
							|  |  |  |             sand_canyon_5 = self.get_region("Sand Canyon 5 - 9") | 
					
						
							|  |  |  |             # this is primarily for typing, but if this ever hits it's fine to crash | 
					
						
							|  |  |  |             assert isinstance(sand_canyon_5, KDL3Room) | 
					
						
							|  |  |  |             cutter_enemy = self.get_restrictive_copy_ability_placement("Cutter Ability", enemies_to_set, | 
					
						
							|  |  |  |                                                                        sand_canyon_5.level, sand_canyon_5.stage) | 
					
						
							|  |  |  |             if cutter_enemy: | 
					
						
							|  |  |  |                 self.copy_abilities[cutter_enemy] = "Cutter Ability" | 
					
						
							|  |  |  |                 enemies_to_set.remove(cutter_enemy) | 
					
						
							|  |  |  |             iceberg_4 = self.get_region("Iceberg 4 - 7") | 
					
						
							|  |  |  |             # this is primarily for typing, but if this ever hits it's fine to crash | 
					
						
							|  |  |  |             assert isinstance(iceberg_4, KDL3Room) | 
					
						
							|  |  |  |             burning_enemy = self.get_restrictive_copy_ability_placement("Burning Ability", enemies_to_set, | 
					
						
							|  |  |  |                                                                         iceberg_4.level, iceberg_4.stage) | 
					
						
							|  |  |  |             if burning_enemy: | 
					
						
							|  |  |  |                 self.copy_abilities[burning_enemy] = "Burning Ability" | 
					
						
							|  |  |  |                 enemies_to_set.remove(burning_enemy) | 
					
						
							| 
									
										
										
										
											2024-03-13 15:05:38 -05:00
										 |  |  |             # ensure we place one of every ability | 
					
						
							|  |  |  |             if unplaced_abilities and self.options.accessibility != self.options.accessibility.option_minimal: | 
					
						
							|  |  |  |                 # failsafe, on non-minimal we need to guarantee every copy ability exists | 
					
						
							|  |  |  |                 for ability in sorted(unplaced_abilities): | 
					
						
							|  |  |  |                     enemy = self.random.choice(enemies_to_set) | 
					
						
							|  |  |  |                     self.copy_abilities[enemy] = ability | 
					
						
							|  |  |  |                     enemies_to_set.remove(enemy) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             # place remaining | 
					
						
							|  |  |  |             for enemy in enemies_to_set: | 
					
						
							|  |  |  |                 self.copy_abilities[enemy] = self.random.choice(valid_abilities) | 
					
						
							|  |  |  |         for enemy in enemy_mapping: | 
					
						
							|  |  |  |             self.multiworld.get_location(enemy, self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item(self.create_item(self.copy_abilities[enemy_mapping[enemy]])) | 
					
						
							|  |  |  |         # fill animals | 
					
						
							|  |  |  |         if self.options.animal_randomization != 0: | 
					
						
							|  |  |  |             spawns = [animal for animal in animal_friend_spawns.keys() if | 
					
						
							|  |  |  |                       animal not in ["Ripple Field 5 - Animal 2", "Sand Canyon 6 - Animal 1", "Iceberg 4 - Animal 1"]] | 
					
						
							|  |  |  |             self.multiworld.get_location("Iceberg 4 - Animal 1", self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item(self.create_item("ChuChu Spawn")) | 
					
						
							|  |  |  |             # Not having ChuChu here makes the room impossible (since only she has vertical burning) | 
					
						
							|  |  |  |             self.multiworld.get_location("Ripple Field 5 - Animal 2", self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item(self.create_item("Pitch Spawn")) | 
					
						
							|  |  |  |             guaranteed_animal = self.random.choice(["Kine Spawn", "Coo Spawn"]) | 
					
						
							|  |  |  |             self.multiworld.get_location("Sand Canyon 6 - Animal 1", self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item(self.create_item(guaranteed_animal)) | 
					
						
							|  |  |  |             # Ripple Field 5 - Animal 2 needs to be Pitch to ensure accessibility on non-door rando | 
					
						
							|  |  |  |             if self.options.animal_randomization == 1: | 
					
						
							|  |  |  |                 animal_pool = [animal_friend_spawns[spawn] for spawn in animal_friend_spawns | 
					
						
							|  |  |  |                                if spawn not in ["Ripple Field 5 - Animal 2", "Sand Canyon 6 - Animal 1", | 
					
						
							|  |  |  |                                                 "Iceberg 4 - Animal 1"]] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"] | 
					
						
							|  |  |  |                 animal_pool = [self.random.choice(animal_base) | 
					
						
							| 
									
										
										
										
											2024-09-04 08:19:00 -05:00
										 |  |  |                                for _ in range(len(animal_friend_spawns) - 10)] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 # have to guarantee one of each animal | 
					
						
							|  |  |  |                 animal_pool.extend(animal_base) | 
					
						
							|  |  |  |             if guaranteed_animal == "Kine Spawn": | 
					
						
							|  |  |  |                 animal_pool.append("Coo Spawn") | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 animal_pool.append("Kine Spawn") | 
					
						
							|  |  |  |             locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             items: List[Item] = [self.create_item(animal) for animal in animal_pool] | 
					
						
							|  |  |  |             allstate = CollectionState(self.multiworld) | 
					
						
							|  |  |  |             for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]: | 
					
						
							|  |  |  |                 self.collect(allstate, self.create_item(item)) | 
					
						
							| 
									
										
										
										
											2024-04-01 08:02:26 -05:00
										 |  |  |             self.random.shuffle(locations) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             fill_restrictive(self.multiworld, allstate, locations, items, True, True) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |             # 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 and x not in animals]) | 
					
						
							|  |  |  |                             spawn_location.item = None | 
					
						
							|  |  |  |                             spawn_location.place_locked_item(self.create_item(new_animal)) | 
					
						
							|  |  |  |                             animals.append(new_animal) | 
					
						
							|  |  |  |                             # logically, this should be sound pre-ER. May need to adjust around it with ER in the future | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         else: | 
					
						
							|  |  |  |             animal_friends = animal_friend_spawns.copy() | 
					
						
							|  |  |  |             for animal in animal_friends: | 
					
						
							|  |  |  |                 self.multiworld.get_location(animal, self.player) \ | 
					
						
							|  |  |  |                     .place_locked_item(self.create_item(animal_friends[animal])) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def create_items(self) -> None: | 
					
						
							|  |  |  |         itempool = [] | 
					
						
							|  |  |  |         itempool.extend([self.create_item(name) for name in copy_ability_table]) | 
					
						
							|  |  |  |         itempool.extend([self.create_item(name) for name in animal_friend_table]) | 
					
						
							|  |  |  |         required_percentage = self.options.heart_stars_required / 100.0 | 
					
						
							|  |  |  |         remaining_items = len(location_table) - len(itempool) | 
					
						
							|  |  |  |         if not self.options.consumables: | 
					
						
							|  |  |  |             remaining_items -= len(consumable_locations) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         if not self.options.starsanity: | 
					
						
							|  |  |  |             remaining_items -= len(star_locations) | 
					
						
							|  |  |  |         max_heart_stars = self.options.max_heart_stars.value | 
					
						
							|  |  |  |         if max_heart_stars > remaining_items: | 
					
						
							|  |  |  |             max_heart_stars = remaining_items | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         # ensure at least 1 heart star required per world | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99) | 
					
						
							|  |  |  |         filler_items = remaining_items - required_heart_stars | 
					
						
							|  |  |  |         converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0)) | 
					
						
							|  |  |  |         non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars | 
					
						
							|  |  |  |         filler_items -= non_required_heart_stars | 
					
						
							|  |  |  |         trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         filler_items -= trap_amount | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         self.required_heart_stars = required_heart_stars | 
					
						
							|  |  |  |         # handle boss requirements here | 
					
						
							|  |  |  |         requirements = [required_heart_stars] | 
					
						
							|  |  |  |         quotient = required_heart_stars // 5  # since we set the last manually, we can afford imperfect rounding | 
					
						
							|  |  |  |         if self.options.boss_requirement_random: | 
					
						
							|  |  |  |             for i in range(1, 5): | 
					
						
							|  |  |  |                 if self.options.strict_bosses: | 
					
						
							|  |  |  |                     max_stars = quotient * i | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     max_stars = required_heart_stars | 
					
						
							|  |  |  |                 requirements.insert(i, self.random.randint( | 
					
						
							|  |  |  |                     min(1, max_stars), max_stars)) | 
					
						
							|  |  |  |             if self.options.strict_bosses: | 
					
						
							|  |  |  |                 requirements.sort() | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.random.shuffle(requirements) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             for i in range(1, 5): | 
					
						
							|  |  |  |                 requirements.insert(i - 1, quotient * i) | 
					
						
							|  |  |  |         self.boss_requirements = requirements | 
					
						
							|  |  |  |         itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value))) | 
					
						
							|  |  |  |                          for _ in range(filler_items)]) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         itempool.extend([self.create_item(self.get_trap_item_name()) | 
					
						
							|  |  |  |                          for _ in range(trap_amount)]) | 
					
						
							|  |  |  |         itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) | 
					
						
							|  |  |  |         self.multiworld.itempool += itempool | 
					
						
							|  |  |  |         if self.options.open_world: | 
					
						
							|  |  |  |             for level in self.player_levels: | 
					
						
							|  |  |  |                 for stage in range(0, 6): | 
					
						
							|  |  |  |                     self.multiworld.get_location(location_table[self.player_levels[level][stage]] | 
					
						
							|  |  |  |                                                  .replace("Complete", "Stage Completion"), self.player) \ | 
					
						
							|  |  |  |                         .place_locked_item(KDL3Item( | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                             f"{location_name.level_names_inverse[level]} - Stage Completion", | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                             ItemClassification.progression, None, self.player)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     set_rules = set_rules | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def generate_basic(self) -> None: | 
					
						
							|  |  |  |         self.stage_shuffle_enabled = self.options.stage_shuffle > 0 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         goal = self.options.goal.value | 
					
						
							|  |  |  |         goal_location = self.multiworld.get_location(location_name.goals[goal], self.player) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player)) | 
					
						
							|  |  |  |         for level in range(1, 6): | 
					
						
							|  |  |  |             self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item( | 
					
						
							|  |  |  |                 KDL3Item(f"Level {level} Boss Defeated", ItemClassification.progression, None, self.player)) | 
					
						
							|  |  |  |             self.multiworld.get_location(f"Level {level} Boss - Purified", self.player) \ | 
					
						
							|  |  |  |                 .place_locked_item( | 
					
						
							|  |  |  |                 KDL3Item(f"Level {level} Boss Purified", ItemClassification.progression, None, self.player)) | 
					
						
							|  |  |  |         self.multiworld.completion_condition[self.player] = lambda state: state.has("Love-Love Rod", self.player) | 
					
						
							|  |  |  |         # this can technically be done at any point before generate_output | 
					
						
							|  |  |  |         if self.options.allow_bb: | 
					
						
							|  |  |  |             if self.options.allow_bb == self.options.allow_bb.option_enforced: | 
					
						
							|  |  |  |                 self.boss_butch_bosses = [True for _ in range(6)] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.boss_butch_bosses = [self.random.choice([True, False]) for _ in range(6)] | 
					
						
							| 
									
										
										
										
											2024-03-13 15:05:38 -05:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.boss_butch_bosses = [False for _ in range(6)] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def generate_output(self, output_directory: str) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             patch = KDL3ProcedurePatch() | 
					
						
							|  |  |  |             patch_rom(self, patch) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             self.rom_name = patch.name | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             patch.write(os.path.join(output_directory, | 
					
						
							|  |  |  |                                      f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         except Exception: | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  |         finally: | 
					
						
							|  |  |  |             self.rom_name_available_event.set()  # make sure threading continues and errors are collected | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def modify_multidata(self, multidata: Dict[str, Any]) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         # wait for self.rom_name to be available. | 
					
						
							|  |  |  |         self.rom_name_available_event.wait() | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |         assert isinstance(self.rom_name, bytes) | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         rom_name = getattr(self, "rom_name", None) | 
					
						
							|  |  |  |         # we skip in case of error, so that the original error in the output thread is the one that gets raised | 
					
						
							|  |  |  |         if rom_name: | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             new_name = base64.b64encode(self.rom_name).decode() | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def fill_slot_data(self) -> Mapping[str, Any]: | 
					
						
							|  |  |  |         # UT support | 
					
						
							|  |  |  |         return {"player_levels": self.player_levels} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def interpret_slot_data(self, slot_data: Mapping[str, Any]): | 
					
						
							|  |  |  |         # UT support | 
					
						
							|  |  |  |         player_levels = {int(key): value for key, value in slot_data["player_levels"].items()} | 
					
						
							|  |  |  |         return {"player_levels": player_levels} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |     def write_spoiler(self, spoiler_handle: TextIO) -> None: | 
					
						
							|  |  |  |         if self.stage_shuffle_enabled: | 
					
						
							|  |  |  |             spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             for level in location_name.level_names: | 
					
						
							|  |  |  |                 for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)): | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                     spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n") | 
					
						
							|  |  |  |         if self.options.animal_randomization: | 
					
						
							|  |  |  |             spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             for lvl in self.player_levels: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                 for stage in range(6): | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                     rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage] | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                     animals = [] | 
					
						
							|  |  |  |                     for room in rooms: | 
					
						
							|  |  |  |                         animals.extend([location.item.name.replace(" Spawn", "") | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                                         for location in room.locations if "Animal" in location.name | 
					
						
							|  |  |  |                                         and location.item is not None]) | 
					
						
							|  |  |  |                     spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}" | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                                          f": {', '.join(animals)}\n") | 
					
						
							|  |  |  |         if self.options.copy_ability_randomization: | 
					
						
							|  |  |  |             spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") | 
					
						
							|  |  |  |             for enemy in self.copy_abilities: | 
					
						
							|  |  |  |                 spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |     def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |         if self.stage_shuffle_enabled: | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |             regions = {location_name.level_names[level]: level for level in location_name.level_names} | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |             level_hint_data = {} | 
					
						
							|  |  |  |             for level in regions: | 
					
						
							|  |  |  |                 for stage in range(7): | 
					
						
							|  |  |  |                     stage_name = self.multiworld.get_location(self.location_id_to_name[self.player_levels[level][stage]], | 
					
						
							|  |  |  |                                                               self.player).name.replace(" - Complete", "") | 
					
						
							|  |  |  |                     stage_regions = [room for room in self.rooms if stage_name in room.name] | 
					
						
							|  |  |  |                     for region in stage_regions: | 
					
						
							| 
									
										
										
										
											2024-08-31 06:15:00 -05:00
										 |  |  |                         for location in [location for location in list(region.get_locations()) if location.address]: | 
					
						
							| 
									
										
										
										
											2024-03-05 09:59:55 -06:00
										 |  |  |                             level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}" | 
					
						
							|  |  |  |             hint_data[self.player] = level_hint_data |