690 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			690 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| Functions related to pokemon species and moves
 | |
| """
 | |
| import functools
 | |
| from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
 | |
| 
 | |
| from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data)
 | |
| from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
 | |
|                       RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
 | |
|                       TmTutorCompatibility)
 | |
| from .util import bool_array_to_int, get_easter_egg, int_to_bool_array
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from random import Random
 | |
|     from . import PokemonEmeraldWorld
 | |
| 
 | |
| 
 | |
| _DAMAGING_MOVES = frozenset({
 | |
|       1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  13,
 | |
|      16,  17,  20,  21,  22,  23,  24,  25,  26,  27,  29,  30,
 | |
|      31,  33,  34,  35,  36,  37,  38,  40,  41,  42,  44,  51,
 | |
|      52,  53,  55,  56,  58,  59,  60,  61,  62,  63,  64,  65,
 | |
|      66,  67,  69,  71,  72,  75,  76,  80,  82,  83,  84,  85,
 | |
|      87,  88,  89,  91,  93,  94,  98,  99, 101, 121, 122, 123,
 | |
|     124, 125, 126, 128, 129, 130, 131, 132, 136, 140, 141, 143,
 | |
|     145, 146, 149, 152, 154, 155, 157, 158, 161, 162, 163, 167,
 | |
|     168, 172, 175, 177, 179, 181, 183, 185, 188, 189, 190, 192,
 | |
|     196, 198, 200, 202, 205, 209, 210, 211, 216, 217, 218, 221,
 | |
|     222, 223, 224, 225, 228, 229, 231, 232, 233, 237, 238, 239,
 | |
|     242, 245, 246, 247, 248, 250, 251, 253, 257, 263, 265, 267,
 | |
|     276, 279, 280, 282, 284, 290, 292, 295, 296, 299, 301, 302,
 | |
|     304, 305, 306, 307, 308, 309, 310, 311, 314, 315, 317, 318,
 | |
|     323, 324, 325, 326, 327, 328, 330, 331, 332, 333, 337, 338,
 | |
|     340, 341, 342, 343, 344, 345, 348, 350, 351, 352, 353, 354,
 | |
| })
 | |
| """IDs for moves that safely deal direct damage, for avoiding putting the
 | |
| player in a situation where they can only use status moves, or are forced
 | |
| to faint themselves, or something of that nature."""
 | |
| 
 | |
| _MOVE_TYPES = [
 | |
|      0,  0,  1,  0,  0,  0,  0, 10, 15, 13,  0,  0,  0,  0,  0,
 | |
|      0,  2,  2,  0,  2,  0,  0, 12,  0,  1,  0,  1,  1,  4,  0,
 | |
|      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  3,  6,  6,  0, 17,
 | |
|      0,  0,  0,  0,  0,  0,  3, 10, 10, 15, 11, 11, 11, 15, 15,
 | |
|     14, 11, 15,  0,  2,  2,  1,  1,  1,  1,  0, 12, 12, 12,  0,
 | |
|     12, 12,  3, 12, 12, 12,  6, 16, 10, 13, 13, 13, 13,  5,  4,
 | |
|      4,  4,  3, 14, 14, 14, 14, 14,  0,  0, 14,  7,  0,  0,  0,
 | |
|      0,  0,  0,  0,  7, 11,  0, 14, 14, 15, 14,  0,  0,  0,  2,
 | |
|      0,  0,  7,  3,  3,  4, 10, 11, 11,  0,  0,  0,  0, 14, 14,
 | |
|      0,  1,  0, 14,  3,  0,  6,  0,  2,  0, 11,  0, 12,  0, 14,
 | |
|      0,  3, 11,  0,  0,  4, 14,  5,  0,  0,  0,  0,  0,  0,  0,
 | |
|      0,  0,  1, 17,  6,  0,  7, 10,  0,  9,  0,  0,  2, 12,  1,
 | |
|      7, 15,  0,  1,  0, 17,  0,  0,  3,  4, 11,  4, 13,  0,  7,
 | |
|      0, 15,  1,  4,  0, 16,  5, 12,  0,  0,  5,  0,  0,  0, 13,
 | |
|      6,  8,  0,  0,  0,  0,  0,  0,  0,  0,  0, 10,  4,  1,  6,
 | |
|     16,  0,  0, 17,  0,  0,  8,  8,  1,  0, 12,  0,  0,  1, 16,
 | |
|     11, 10, 17, 14,  0,  0,  5,  7, 14,  1, 11, 17,  0,  0,  0,
 | |
|      0,  0, 10, 15, 17, 17, 10, 17,  0,  1,  0,  0,  0, 13, 17,
 | |
|      0, 14, 14,  0,  0, 12,  1, 14,  0,  1,  1,  0, 17,  0, 10,
 | |
|     14, 14,  0,  7, 17,  0, 11,  1,  0,  6, 14, 14,  2,  0, 10,
 | |
|      4, 15, 12,  0,  0,  3,  0, 10, 11,  8,  7,  0, 12, 17,  2,
 | |
|     10,  0,  5,  6,  8, 12,  0, 14, 11,  6,  7, 14,  1,  4, 15,
 | |
|     11, 12,  2, 15,  8,  0,  0, 16, 12,  1,  2,  4,  3,  0, 13,
 | |
|     12, 11, 14, 12, 16,  5, 13, 11,  8, 14,
 | |
| ]
 | |
| """Maps move ids to the type of that move"""
 | |
| 
 | |
| _MOVES_BY_TYPE: Dict[int, List[int]] = {}
 | |
| """Categorizes move ids by their type"""
 | |
| for move, type in enumerate(_MOVE_TYPES):
 | |
|     _MOVES_BY_TYPE.setdefault(type, []).append(move)
 | |
| 
 | |
| HM_MOVES = frozenset({
 | |
|     data.constants["MOVE_CUT"],
 | |
|     data.constants["MOVE_FLY"],
 | |
|     data.constants["MOVE_SURF"],
 | |
|     data.constants["MOVE_STRENGTH"],
 | |
|     data.constants["MOVE_FLASH"],
 | |
|     data.constants["MOVE_ROCK_SMASH"],
 | |
|     data.constants["MOVE_WATERFALL"],
 | |
|     data.constants["MOVE_DIVE"],
 | |
| })
 | |
| 
 | |
| _MOVE_BLACKLIST = frozenset({
 | |
|     0,    # MOVE_NONE
 | |
|     165,  # Struggle
 | |
| } | HM_MOVES)
 | |
| 
 | |
| 
 | |
| @functools.lru_cache(maxsize=386)
 | |
| def get_species_id_by_label(label: str) -> int:
 | |
|     return next(species.species_id for species in data.species.values() if species.label == label)
 | |
| 
 | |
| 
 | |
| def get_random_type(random: "Random") -> int:
 | |
|     picked_type = random.randrange(0, 18)
 | |
|     while picked_type == 9:  # Don't pick the ??? type
 | |
|         picked_type = random.randrange(0, 18)
 | |
| 
 | |
|     return picked_type
 | |
| 
 | |
| 
 | |
| def get_random_move(
 | |
|         random: "Random",
 | |
|         blacklist: Optional[Set[int]] = None,
 | |
|         type_bias: int = 0,
 | |
|         normal_bias: int = 0,
 | |
|         type_target: Optional[Tuple[int, int]] = None) -> int:
 | |
|     expanded_blacklist = _MOVE_BLACKLIST | (blacklist if blacklist is not None else set())
 | |
| 
 | |
|     bias = random.random() * 100
 | |
|     if bias < type_bias:
 | |
|         pass  # Keep type_target unchanged
 | |
|     elif bias < type_bias + ((100 - type_bias) * (normal_bias / 100)):
 | |
|         type_target = (0, 0)
 | |
|     else:
 | |
|         type_target = None
 | |
| 
 | |
|     chosen_move = None
 | |
| 
 | |
|     # The blacklist is relatively small, so if we don't need to restrict
 | |
|     # ourselves to any particular types, it's usually much faster to pick
 | |
|     # a random number and hope it works. Limit this to 5 tries in case the
 | |
|     # blacklist is actually significant enough to make this unlikely to work.
 | |
|     if type_target is None:
 | |
|         remaining_attempts = 5
 | |
|         while remaining_attempts > 0:
 | |
|             remaining_attempts -= 1
 | |
|             chosen_move = random.randrange(0, data.constants["MOVES_COUNT"])
 | |
|             if chosen_move not in expanded_blacklist:
 | |
|                 return chosen_move
 | |
|         else:
 | |
|             chosen_move = None
 | |
| 
 | |
|     # We're either matching types or failed to pick a move above
 | |
|     if type_target is None:
 | |
|         possible_moves = [i for i in range(data.constants["MOVES_COUNT"]) if i not in expanded_blacklist]
 | |
|     else:
 | |
|         possible_moves = [move for move in _MOVES_BY_TYPE[type_target[0]] if move not in expanded_blacklist] + \
 | |
|                          [move for move in _MOVES_BY_TYPE[type_target[1]] if move not in expanded_blacklist]
 | |
| 
 | |
|     if len(possible_moves) == 0:
 | |
|         return get_random_move(random, None, type_bias, normal_bias, type_target)
 | |
| 
 | |
|     return random.choice(possible_moves)
 | |
| 
 | |
| 
 | |
| def get_random_damaging_move(random: "Random", blacklist: Optional[Set[int]] = None) -> int:
 | |
|     expanded_blacklist = _MOVE_BLACKLIST | (blacklist if blacklist is not None else set())
 | |
|     move_options = list(_DAMAGING_MOVES)
 | |
| 
 | |
|     move = random.choice(move_options)
 | |
|     while move in expanded_blacklist:
 | |
|         move = random.choice(move_options)
 | |
| 
 | |
|     return move
 | |
| 
 | |
| 
 | |
| def filter_species_by_nearby_bst(species: List[SpeciesData], target_bst: int) -> List[SpeciesData]:
 | |
|     # Sort by difference in bst, then chop off the tail of the list that's more than
 | |
|     # 10% different. If that leaves the list empty, increase threshold to 20%, then 30%, etc.
 | |
|     species = sorted(species, key=lambda species: abs(sum(species.base_stats) - target_bst))
 | |
|     cutoff_index = 0
 | |
|     max_percent_different = 10
 | |
|     while cutoff_index == 0 and max_percent_different < 10000:
 | |
|         while cutoff_index < len(species) and abs(sum(species[cutoff_index].base_stats) - target_bst) < target_bst * (max_percent_different / 100):
 | |
|             cutoff_index += 1
 | |
|         max_percent_different += 10
 | |
| 
 | |
|     return species[:cutoff_index + 1]
 | |
| 
 | |
| 
 | |
| def randomize_types(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.types == RandomizeTypes.option_shuffle:
 | |
|         type_map = list(range(18))
 | |
|         world.random.shuffle(type_map)
 | |
| 
 | |
|         # We never want to map to the ??? type, so swap whatever index maps to ??? with ???
 | |
|         # which forces ??? to always map to itself. There are no pokemon which have the ??? type
 | |
|         mystery_type_index = type_map.index(9)
 | |
|         type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
 | |
| 
 | |
|         for species in world.modified_species.values():
 | |
|             species.types = (type_map[species.types[0]], type_map[species.types[1]])
 | |
|     elif world.options.types == RandomizeTypes.option_completely_random:
 | |
|         for species in world.modified_species.values():
 | |
|             new_type_1 = get_random_type(world.random)
 | |
|             new_type_2 = new_type_1
 | |
|             if species.types[0] != species.types[1]:
 | |
|                 while new_type_1 == new_type_2:
 | |
|                     new_type_2 = get_random_type(world.random)
 | |
| 
 | |
|             species.types = (new_type_1, new_type_2)
 | |
|     elif world.options.types == RandomizeTypes.option_follow_evolutions:
 | |
|         already_modified: Set[int] = set()
 | |
| 
 | |
|         # Similar to follow evolutions for abilities, but only needs to loop through once.
 | |
|         # For every pokemon without a pre-evolution, generates a random mapping from old types to new types
 | |
|         # and then walks through the evolution tree applying that map. This means that evolutions that share
 | |
|         # types will have those types mapped to the same new types, and evolutions with new or diverging types
 | |
|         # will still have new or diverging types.
 | |
|         # Consider:
 | |
|         # - Charmeleon (Fire/Fire) -> Charizard (Fire/Flying)
 | |
|         # - Onyx (Rock/Ground) -> Steelix (Steel/Ground)
 | |
|         # - Nincada (Bug/Ground) -> Ninjask (Bug/Flying) && Shedinja (Bug/Ghost)
 | |
|         # - Azurill (Normal/Normal) -> Marill (Water/Water)
 | |
|         for species in world.modified_species.values():
 | |
|             if species.species_id in already_modified:
 | |
|                 continue
 | |
|             if species.pre_evolution is not None and species.pre_evolution not in already_modified:
 | |
|                 continue
 | |
| 
 | |
|             type_map = list(range(18))
 | |
|             world.random.shuffle(type_map)
 | |
| 
 | |
|             # We never want to map to the ??? type, so swap whatever index maps to ??? with ???
 | |
|             # which forces ??? to always map to itself. There are no pokemon which have the ??? type
 | |
|             mystery_type_index = type_map.index(9)
 | |
|             type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
 | |
| 
 | |
|             evolutions = [species]
 | |
|             while len(evolutions) > 0:
 | |
|                 evolution = evolutions.pop()
 | |
|                 evolution.types = (type_map[evolution.types[0]], type_map[evolution.types[1]])
 | |
|                 already_modified.add(evolution.species_id)
 | |
|                 evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
 | |
| 
 | |
| 
 | |
| def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
 | |
|         return
 | |
| 
 | |
|     from collections import defaultdict
 | |
| 
 | |
|     should_match_bst = world.options.wild_pokemon in {
 | |
|         RandomizeWildPokemon.option_match_base_stats,
 | |
|         RandomizeWildPokemon.option_match_base_stats_and_type,
 | |
|     }
 | |
|     should_match_type = world.options.wild_pokemon in {
 | |
|         RandomizeWildPokemon.option_match_type,
 | |
|         RandomizeWildPokemon.option_match_base_stats_and_type,
 | |
|     }
 | |
| 
 | |
|     already_placed = set()
 | |
|     num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds)
 | |
| 
 | |
|     priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]]
 | |
| 
 | |
|     # Loop over map data to modify their encounter slots
 | |
|     map_names = list(world.modified_maps.keys())
 | |
|     world.random.shuffle(map_names)
 | |
|     for map_name in map_names:
 | |
|         placed_priority_species = False
 | |
|         map_data = world.modified_maps[map_name]
 | |
| 
 | |
|         new_encounters: List[Optional[EncounterTableData]] = [None, None, None]
 | |
|         old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
 | |
| 
 | |
|         for i, table in enumerate(old_encounters):
 | |
|             if table is not None:
 | |
|                 # Create a map from the original species to new species
 | |
|                 # instead of just randomizing every slot.
 | |
|                 # Force area 1-to-1 mapping, in other words.
 | |
|                 species_old_to_new_map: Dict[int, int] = {}
 | |
|                 for species_id in table.slots:
 | |
|                     if species_id not in species_old_to_new_map:
 | |
|                         if not placed_priority_species and len(priority_species) > 0 \
 | |
|                                 and map_name not in OUT_OF_LOGIC_MAPS:
 | |
|                             new_species_id = priority_species.pop()
 | |
|                             placed_priority_species = True
 | |
|                         else:
 | |
|                             original_species = data.species[species_id]
 | |
| 
 | |
|                             # Construct progressive tiers of blacklists that can be peeled back if they
 | |
|                             # collectively cover too much of the pokedex. A lower index in `blacklists`
 | |
|                             # indicates a more important set of species to avoid. Entries at `0` will
 | |
|                             # always be blacklisted.
 | |
|                             blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
 | |
| 
 | |
|                             # Blacklist pokemon already on this table
 | |
|                             blacklists[0].append(set(species_old_to_new_map.values()))
 | |
| 
 | |
|                             # If doing legendary hunt, blacklist Latios from wild encounters so
 | |
|                             # it can be tracked as the roamer. Otherwise it may be impossible
 | |
|                             # to tell whether a highlighted route is the roamer or a wild
 | |
|                             # encounter.
 | |
|                             if world.options.goal == Goal.option_legendary_hunt:
 | |
|                                 blacklists[0].append({data.constants["SPECIES_LATIOS"]})
 | |
| 
 | |
|                             # If dexsanity/catch 'em all mode, blacklist already placed species
 | |
|                             # until every species has been placed once
 | |
|                             if world.options.dexsanity and len(already_placed) < num_placeable_species:
 | |
|                                 blacklists[1].append(already_placed)
 | |
| 
 | |
|                             # Blacklist from player options
 | |
|                             blacklists[2].append(world.blacklisted_wilds)
 | |
| 
 | |
|                             # Type matching blacklist
 | |
|                             if should_match_type:
 | |
|                                 blacklists[3].append({
 | |
|                                     species.species_id
 | |
|                                     for species in world.modified_species.values()
 | |
|                                     if not bool(set(species.types) & set(original_species.types))
 | |
|                                 })
 | |
| 
 | |
|                             merged_blacklist: Set[int] = set()
 | |
|                             for max_priority in reversed(sorted(blacklists.keys())):
 | |
|                                 merged_blacklist = set()
 | |
|                                 for priority in blacklists.keys():
 | |
|                                     if priority <= max_priority:
 | |
|                                         for blacklist in blacklists[priority]:
 | |
|                                             merged_blacklist |= blacklist
 | |
| 
 | |
|                                 if len(merged_blacklist) < NUM_REAL_SPECIES:
 | |
|                                     break
 | |
|                             else:
 | |
|                                 raise RuntimeError("This should never happen")
 | |
| 
 | |
|                             candidates = [
 | |
|                                 species
 | |
|                                 for species in world.modified_species.values()
 | |
|                                 if species.species_id not in merged_blacklist
 | |
|                             ]
 | |
| 
 | |
|                             if should_match_bst:
 | |
|                                 candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
 | |
| 
 | |
|                             new_species_id = world.random.choice(candidates).species_id
 | |
|                         species_old_to_new_map[species_id] = new_species_id
 | |
| 
 | |
|                         if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS:
 | |
|                             already_placed.add(new_species_id)
 | |
| 
 | |
|                 # Actually create the new list of slots and encounter table
 | |
|                 new_slots: List[int] = []
 | |
|                 for species_id in table.slots:
 | |
|                     new_slots.append(species_old_to_new_map[species_id])
 | |
| 
 | |
|                 new_encounters[i] = EncounterTableData(new_slots, table.address)
 | |
| 
 | |
|                 # Rename event items for the new wild pokemon species
 | |
|                 slot_category: Tuple[str, List[Tuple[Optional[str], range]]] = [
 | |
|                     ("LAND", [(None, range(0, 12))]),
 | |
|                     ("WATER", [(None, range(0, 5))]),
 | |
|                     ("FISHING", [("OLD_ROD", range(0, 2)), ("GOOD_ROD", range(2, 5)), ("SUPER_ROD", range(5, 10))]),
 | |
|                 ][i]
 | |
|                 for j, new_species_id in enumerate(new_slots):
 | |
|                     # Get the subcategory for rods
 | |
|                     subcategory = next(sc for sc in slot_category[1] if j in sc[1])
 | |
|                     subcategory_species = []
 | |
|                     for k in subcategory[1]:
 | |
|                         if new_slots[k] not in subcategory_species:
 | |
|                             subcategory_species.append(new_slots[k])
 | |
| 
 | |
|                     # Create the name of the location that corresponds to this encounter slot
 | |
|                     # Fishing locations include the rod name
 | |
|                     subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0]
 | |
|                     encounter_location_index = subcategory_species.index(new_species_id) + 1
 | |
|                     encounter_location_name = f"{map_data.name}_{slot_category[0]}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
 | |
|                     try:
 | |
|                         # Get the corresponding location and change the event name to reflect the new species
 | |
|                         slot_location = world.multiworld.get_location(encounter_location_name, world.player)
 | |
|                         slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
 | |
|                     except KeyError:
 | |
|                         pass  # Map probably isn't included; should be careful here about bad encounter location names
 | |
| 
 | |
|         map_data.land_encounters = new_encounters[0]
 | |
|         map_data.water_encounters = new_encounters[1]
 | |
|         map_data.fishing_encounters = new_encounters[2]
 | |
| 
 | |
| 
 | |
| def randomize_abilities(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.abilities == RandomizeAbilities.option_vanilla:
 | |
|         return
 | |
| 
 | |
|     # Creating list of potential abilities
 | |
|     ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in data.abilities}
 | |
| 
 | |
|     ability_blacklist_labels = {"cacophony"}  # Cacophony is defined and has a description, but no effect
 | |
|     option_ability_blacklist = world.options.ability_blacklist.value
 | |
|     if option_ability_blacklist is not None:
 | |
|         ability_blacklist_labels |= {ability_label.lower() for ability_label in option_ability_blacklist}
 | |
| 
 | |
|     ability_blacklist = {ability_label_to_value[label] for label in ability_blacklist_labels}
 | |
|     ability_whitelist = [a.ability_id for a in data.abilities if a.ability_id not in ability_blacklist]
 | |
| 
 | |
|     if world.options.abilities == RandomizeAbilities.option_follow_evolutions:
 | |
|         already_modified: Set[int] = set()
 | |
| 
 | |
|         # Loops through species and only tries to modify abilities if the pokemon has no pre-evolution
 | |
|         # or if the pre-evolution has already been modified. Then tries to modify all species that evolve
 | |
|         # from this one which have the same abilities.
 | |
|         #
 | |
|         # The outer while loop only runs three times for vanilla ordering: Once for a first pass, once for
 | |
|         # Hitmonlee/Hitmonchan, and once to verify that there's nothing left to do.
 | |
|         while True:
 | |
|             had_clean_pass = True
 | |
|             for species in world.modified_species.values():
 | |
|                 if species.species_id in already_modified:
 | |
|                     continue
 | |
|                 if species.pre_evolution is not None and species.pre_evolution not in already_modified:
 | |
|                     continue
 | |
| 
 | |
|                 had_clean_pass = False
 | |
| 
 | |
|                 old_abilities = species.abilities
 | |
|                 # 0 is the value for "no ability"; species with only 1 ability have the other set to 0
 | |
|                 new_abilities = (
 | |
|                     0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
 | |
|                     0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
 | |
|                 )
 | |
| 
 | |
|                 # Recursively modify the abilities of anything that evolves from this pokemon
 | |
|                 # until the evolution doesn't have a matching set of abilities
 | |
|                 evolutions = [species]
 | |
|                 while len(evolutions) > 0:
 | |
|                     evolution = evolutions.pop()
 | |
|                     if evolution.abilities == old_abilities:
 | |
|                         evolution.abilities = new_abilities
 | |
|                         already_modified.add(evolution.species_id)
 | |
|                         evolutions += [
 | |
|                             world.modified_species[evolution.species_id]
 | |
|                             for evolution in evolution.evolutions
 | |
|                             if evolution.species_id not in already_modified
 | |
|                         ]
 | |
| 
 | |
|             if had_clean_pass:
 | |
|                 break
 | |
|     else:  # Not following evolutions
 | |
|         for species in world.modified_species.values():
 | |
|             old_abilities = species.abilities
 | |
|             # 0 is the value for "no ability"; species with only 1 ability have the other set to 0
 | |
|             new_abilities = (
 | |
|                 0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
 | |
|                 0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
 | |
|             )
 | |
| 
 | |
|             species.abilities = new_abilities
 | |
| 
 | |
| 
 | |
| def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.level_up_moves == LevelUpMoves.option_vanilla:
 | |
|         return
 | |
| 
 | |
|     type_bias = world.options.move_match_type_bias.value
 | |
|     normal_bias = world.options.move_normal_type_bias.value
 | |
| 
 | |
|     for species in world.modified_species.values():
 | |
|         old_learnset = species.learnset
 | |
|         new_learnset: List[LearnsetMove] = []
 | |
| 
 | |
|         # All species have 4 moves at level 0. Up to 3 of them are blank spaces reserved for the
 | |
|         # start with four moves option. This either replaces those moves or leaves it blank
 | |
|         # and moves the cursor.
 | |
|         cursor = 0
 | |
|         while old_learnset[cursor].move_id == 0:
 | |
|             if world.options.level_up_moves == LevelUpMoves.option_start_with_four_moves:
 | |
|                 new_move = get_random_move(world.random,
 | |
|                                            {move.move_id for move in new_learnset} | world.blacklisted_moves,
 | |
|                                            type_bias, normal_bias, species.types)
 | |
|             else:
 | |
|                 new_move = 0
 | |
|             new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
 | |
|             cursor += 1
 | |
| 
 | |
|         # All moves from here onward are actual moves.
 | |
|         while cursor < len(old_learnset):
 | |
|             # Guarantees the starter has a good damaging move; i will always be <=3 when entering this loop
 | |
|             if cursor == 3:
 | |
|                 new_move = get_random_damaging_move(world.random, {move.move_id for move in new_learnset})
 | |
|             else:
 | |
|                 new_move = get_random_move(world.random,
 | |
|                                            {move.move_id for move in new_learnset} | world.blacklisted_moves,
 | |
|                                            type_bias, normal_bias, species.types)
 | |
|             new_learnset.append(old_learnset[cursor]._replace(move_id=new_move))
 | |
|             cursor += 1
 | |
| 
 | |
|         species.learnset = new_learnset
 | |
| 
 | |
|         
 | |
| def randomize_starters(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.starters == RandomizeStarters.option_vanilla:
 | |
|         return
 | |
| 
 | |
|     should_match_bst = world.options.starters in {
 | |
|         RandomizeStarters.option_match_base_stats,
 | |
|         RandomizeStarters.option_match_base_stats_and_type,
 | |
|     }
 | |
|     should_match_type = world.options.starters in {
 | |
|         RandomizeStarters.option_match_type,
 | |
|         RandomizeStarters.option_match_base_stats_and_type,
 | |
|     }
 | |
| 
 | |
|     new_starters: List[SpeciesData] = []
 | |
| 
 | |
|     easter_egg_type, easter_egg_value = get_easter_egg(world.options.easter_egg.value)
 | |
|     if easter_egg_type == 1:
 | |
|         new_starters = [
 | |
|             world.modified_species[easter_egg_value],
 | |
|             world.modified_species[easter_egg_value],
 | |
|             world.modified_species[easter_egg_value]
 | |
|         ]
 | |
|     else:
 | |
|         for i, starter_id in enumerate(data.starters):
 | |
|             original_starter = data.species[starter_id]
 | |
|             type_blacklist = {
 | |
|                 species.species_id
 | |
|                 for species in world.modified_species.values()
 | |
|                 if not bool(set(species.types) & set(original_starter.types))
 | |
|             } if should_match_type else set()
 | |
| 
 | |
|             merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters | type_blacklist
 | |
|             if len(merged_blacklist) == NUM_REAL_SPECIES:
 | |
|                 merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters
 | |
|             if len(merged_blacklist) == NUM_REAL_SPECIES:
 | |
|                 merged_blacklist = set(s.species_id for s in new_starters)
 | |
| 
 | |
|             candidates = [
 | |
|                 species
 | |
|                 for species in world.modified_species.values()
 | |
|                 if species.species_id not in merged_blacklist
 | |
|             ]
 | |
| 
 | |
|             if should_match_bst:
 | |
|                 candidates = filter_species_by_nearby_bst(candidates, sum(original_starter.base_stats))
 | |
| 
 | |
|             new_starters.append(world.random.choice(candidates))
 | |
| 
 | |
|     world.modified_starters = (
 | |
|         new_starters[0].species_id,
 | |
|         new_starters[1].species_id,
 | |
|         new_starters[2].species_id
 | |
|     )
 | |
| 
 | |
|     # Putting the unchosen starter onto the rival's team
 | |
|     # (trainer name, index of starter in team, whether the starter is evolved)
 | |
|     rival_teams: List[List[Tuple[str, int, bool]]] = [
 | |
|         [
 | |
|             ("TRAINER_BRENDAN_ROUTE_103_TREECKO", 0, False),
 | |
|             ("TRAINER_BRENDAN_RUSTBORO_TREECKO",  1, False),
 | |
|             ("TRAINER_BRENDAN_ROUTE_110_TREECKO", 2, True ),
 | |
|             ("TRAINER_BRENDAN_ROUTE_119_TREECKO", 2, True ),
 | |
|             ("TRAINER_BRENDAN_LILYCOVE_TREECKO",  3, True ),
 | |
|             ("TRAINER_MAY_ROUTE_103_TREECKO",     0, False),
 | |
|             ("TRAINER_MAY_RUSTBORO_TREECKO",      1, False),
 | |
|             ("TRAINER_MAY_ROUTE_110_TREECKO",     2, True ),
 | |
|             ("TRAINER_MAY_ROUTE_119_TREECKO",     2, True ),
 | |
|             ("TRAINER_MAY_LILYCOVE_TREECKO",      3, True ),
 | |
|         ],
 | |
|         [
 | |
|             ("TRAINER_BRENDAN_ROUTE_103_TORCHIC", 0, False),
 | |
|             ("TRAINER_BRENDAN_RUSTBORO_TORCHIC",  1, False),
 | |
|             ("TRAINER_BRENDAN_ROUTE_110_TORCHIC", 2, True ),
 | |
|             ("TRAINER_BRENDAN_ROUTE_119_TORCHIC", 2, True ),
 | |
|             ("TRAINER_BRENDAN_LILYCOVE_TORCHIC",  3, True ),
 | |
|             ("TRAINER_MAY_ROUTE_103_TORCHIC",     0, False),
 | |
|             ("TRAINER_MAY_RUSTBORO_TORCHIC",      1, False),
 | |
|             ("TRAINER_MAY_ROUTE_110_TORCHIC",     2, True ),
 | |
|             ("TRAINER_MAY_ROUTE_119_TORCHIC",     2, True ),
 | |
|             ("TRAINER_MAY_LILYCOVE_TORCHIC",      3, True ),
 | |
|         ],
 | |
|         [
 | |
|             ("TRAINER_BRENDAN_ROUTE_103_MUDKIP", 0, False),
 | |
|             ("TRAINER_BRENDAN_RUSTBORO_MUDKIP",  1, False),
 | |
|             ("TRAINER_BRENDAN_ROUTE_110_MUDKIP", 2, True ),
 | |
|             ("TRAINER_BRENDAN_ROUTE_119_MUDKIP", 2, True ),
 | |
|             ("TRAINER_BRENDAN_LILYCOVE_MUDKIP",  3, True ),
 | |
|             ("TRAINER_MAY_ROUTE_103_MUDKIP",     0, False),
 | |
|             ("TRAINER_MAY_RUSTBORO_MUDKIP",      1, False),
 | |
|             ("TRAINER_MAY_ROUTE_110_MUDKIP",     2, True ),
 | |
|             ("TRAINER_MAY_ROUTE_119_MUDKIP",     2, True ),
 | |
|             ("TRAINER_MAY_LILYCOVE_MUDKIP",      3, True ),
 | |
|         ],
 | |
|     ]
 | |
| 
 | |
|     for i, starter in enumerate([new_starters[1], new_starters[2], new_starters[0]]):
 | |
|         potential_evolutions = [evolution.species_id for evolution in starter.evolutions]
 | |
|         picked_evolution = starter.species_id
 | |
|         if len(potential_evolutions) > 0:
 | |
|             picked_evolution = world.random.choice(potential_evolutions)
 | |
| 
 | |
|         for trainer_name, starter_position, is_evolved in rival_teams[i]:
 | |
|             new_species_id = picked_evolution if is_evolved else starter.species_id
 | |
|             trainer_data = world.modified_trainers[data.constants[trainer_name]]
 | |
|             trainer_data.party.pokemon[starter_position] = \
 | |
|                 trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id)
 | |
| 
 | |
| 
 | |
| def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.legendary_encounters == RandomizeLegendaryEncounters.option_vanilla:
 | |
|         return
 | |
|     elif world.options.legendary_encounters == RandomizeLegendaryEncounters.option_shuffle:
 | |
|         # Just take the existing species and shuffle them
 | |
|         shuffled_species = [encounter.species_id for encounter in data.legendary_encounters]
 | |
|         world.random.shuffle(shuffled_species)
 | |
| 
 | |
|         for i, encounter in enumerate(data.legendary_encounters):
 | |
|             world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i]))
 | |
|     else:
 | |
|         should_match_bst = world.options.legendary_encounters in {
 | |
|             RandomizeLegendaryEncounters.option_match_base_stats,
 | |
|             RandomizeLegendaryEncounters.option_match_base_stats_and_type
 | |
|         }
 | |
|         should_match_type = world.options.legendary_encounters in {
 | |
|             RandomizeLegendaryEncounters.option_match_type,
 | |
|             RandomizeLegendaryEncounters.option_match_base_stats_and_type
 | |
|         }
 | |
| 
 | |
|         for encounter in data.legendary_encounters:
 | |
|             original_species = world.modified_species[encounter.species_id]
 | |
| 
 | |
|             candidates = list(world.modified_species.values())
 | |
|             if should_match_type:
 | |
|                 candidates = [
 | |
|                     species
 | |
|                     for species in candidates
 | |
|                     if bool(set(species.types) & set(original_species.types))
 | |
|                 ]
 | |
|             if should_match_bst:
 | |
|                 candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
 | |
| 
 | |
|             world.modified_legendary_encounters.append(encounter._replace(
 | |
|                 species_id=world.random.choice(candidates).species_id
 | |
|             ))
 | |
| 
 | |
| 
 | |
| def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
 | |
|     if world.options.misc_pokemon == RandomizeMiscPokemon.option_vanilla:
 | |
|         return
 | |
|     elif world.options.misc_pokemon == RandomizeMiscPokemon.option_shuffle:
 | |
|         # Just take the existing species and shuffle them
 | |
|         shuffled_species = [encounter.species_id for encounter in data.misc_pokemon]
 | |
|         world.random.shuffle(shuffled_species)
 | |
| 
 | |
|         world.modified_misc_pokemon = []
 | |
|         for i, encounter in enumerate(data.misc_pokemon):
 | |
|             world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i]))
 | |
|     else:
 | |
|         should_match_bst = world.options.misc_pokemon in {
 | |
|             RandomizeMiscPokemon.option_match_base_stats,
 | |
|             RandomizeMiscPokemon.option_match_base_stats_and_type,
 | |
|         }
 | |
|         should_match_type = world.options.misc_pokemon in {
 | |
|             RandomizeMiscPokemon.option_match_type,
 | |
|             RandomizeMiscPokemon.option_match_base_stats_and_type,
 | |
|         }
 | |
| 
 | |
|         for encounter in data.misc_pokemon:
 | |
|             original_species = world.modified_species[encounter.species_id]
 | |
| 
 | |
|             candidates = list(world.modified_species.values())
 | |
|             if should_match_type:
 | |
|                 candidates = [
 | |
|                     species
 | |
|                     for species in candidates
 | |
|                     if bool(set(species.types) & set(original_species.types))
 | |
|                 ]
 | |
|             if should_match_bst:
 | |
|                 candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
 | |
|             
 | |
|             player_filtered_candidates = [
 | |
|                 species
 | |
|                 for species in candidates
 | |
|                 if species.species_id not in world.blacklisted_wilds
 | |
|             ]
 | |
|             if len(player_filtered_candidates) > 0:
 | |
|                 candidates = player_filtered_candidates
 | |
| 
 | |
|             world.modified_misc_pokemon.append(encounter._replace(
 | |
|                 species_id=world.random.choice(candidates).species_id
 | |
|             ))
 | |
| 
 | |
| 
 | |
| def randomize_tm_hm_compatibility(world: "PokemonEmeraldWorld") -> None:
 | |
|     for species in world.modified_species.values():
 | |
|         # TM and HM compatibility is stored as a 64-bit bitfield
 | |
|         combatibility_array = int_to_bool_array(species.tm_hm_compatibility)
 | |
| 
 | |
|         # TMs
 | |
|         if world.options.tm_tutor_compatibility != TmTutorCompatibility.special_range_names["vanilla"]:
 | |
|             for i in range(0, 50):
 | |
|                 combatibility_array[i] = world.random.random() < world.options.tm_tutor_compatibility / 100
 | |
| 
 | |
|         # HMs
 | |
|         if world.options.hm_compatibility != HmCompatibility.special_range_names["vanilla"]:
 | |
|             for i in range(50, 58):
 | |
|                 combatibility_array[i] = world.random.random() < world.options.hm_compatibility / 100
 | |
| 
 | |
|         species.tm_hm_compatibility = bool_array_to_int(combatibility_array)
 | 
