961 lines
46 KiB
Python
961 lines
46 KiB
Python
"""
|
|
Archipelago World definition for Pokemon Emerald Version
|
|
"""
|
|
from collections import Counter
|
|
import copy
|
|
import logging
|
|
import os
|
|
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar
|
|
|
|
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
|
|
from Fill import FillError, fill_restrictive
|
|
from Options import Toggle
|
|
import settings
|
|
from worlds.AutoWorld import WebWorld, World
|
|
|
|
from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
|
|
from .data import (SpeciesData, MapData, EncounterTableData, LearnsetMove, TrainerPokemonData, StaticEncounterData,
|
|
TrainerData, data as emerald_data)
|
|
from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification,
|
|
offset_item_value)
|
|
from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
|
|
create_locations_with_tags)
|
|
from .options import (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
|
|
RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility,
|
|
HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions)
|
|
from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type
|
|
from .regions import create_regions
|
|
from .rom import PokemonEmeraldDeltaPatch, generate_output, location_visited_event_to_id_map
|
|
from .rules import set_rules
|
|
from .sanity_check import validate_regions
|
|
from .util import int_to_bool_array, bool_array_to_int
|
|
|
|
|
|
class PokemonEmeraldWebWorld(WebWorld):
|
|
"""
|
|
Webhost info for Pokemon Emerald
|
|
"""
|
|
theme = "ocean"
|
|
|
|
setup_en = Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to playing Pokémon Emerald with Archipelago.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Zunawe"]
|
|
)
|
|
|
|
setup_es = Tutorial(
|
|
"Guía de configuración para Multiworld",
|
|
"Una guía para jugar Pokémon Emerald en Archipelago",
|
|
"Español",
|
|
"setup_es.md",
|
|
"setup/es",
|
|
["nachocua"]
|
|
)
|
|
|
|
tutorials = [setup_en, setup_es]
|
|
|
|
|
|
class PokemonEmeraldSettings(settings.Group):
|
|
class PokemonEmeraldRomFile(settings.UserFilePath):
|
|
"""File name of your English Pokemon Emerald ROM"""
|
|
description = "Pokemon Emerald ROM File"
|
|
copy_to = "Pokemon - Emerald Version (USA, Europe).gba"
|
|
md5s = [PokemonEmeraldDeltaPatch.hash]
|
|
|
|
rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to)
|
|
|
|
|
|
class PokemonEmeraldWorld(World):
|
|
"""
|
|
Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise.
|
|
Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots
|
|
of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion!
|
|
"""
|
|
game = "Pokemon Emerald"
|
|
web = PokemonEmeraldWebWorld()
|
|
topology_present = True
|
|
|
|
settings_key = "pokemon_emerald_settings"
|
|
settings: ClassVar[PokemonEmeraldSettings]
|
|
|
|
options_dataclass = PokemonEmeraldOptions
|
|
options: PokemonEmeraldOptions
|
|
|
|
item_name_to_id = create_item_label_to_code_map()
|
|
location_name_to_id = create_location_label_to_id_map()
|
|
item_name_groups = ITEM_GROUPS
|
|
location_name_groups = LOCATION_GROUPS
|
|
|
|
data_version = 1
|
|
required_client_version = (0, 4, 3)
|
|
|
|
badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None
|
|
hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None
|
|
free_fly_location_id: int = 0
|
|
|
|
modified_species: List[Optional[SpeciesData]]
|
|
modified_maps: List[MapData]
|
|
modified_tmhm_moves: List[int]
|
|
modified_static_encounters: List[int]
|
|
modified_starters: Tuple[int, int, int]
|
|
modified_trainers: List[TrainerData]
|
|
|
|
@classmethod
|
|
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
|
if not os.path.exists(cls.settings.rom_file):
|
|
raise FileNotFoundError(cls.settings.rom_file)
|
|
|
|
assert validate_regions()
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
return "Great Ball"
|
|
|
|
def generate_early(self) -> None:
|
|
# If badges or HMs are vanilla, Norman locks you from using Surf, which means you're not guaranteed to be
|
|
# able to reach Fortree Gym, Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those gyms to
|
|
# challenge Norman or it creates a circular dependency.
|
|
# This is never a problem for completely random badges/hms because the algo will not place Surf/Balance Badge
|
|
# on Norman on its own. It's never a problem for shuffled badges/hms because there is no scenario where Cut or
|
|
# the Stone Badge can be a lynchpin for access to any gyms, so they can always be put on Norman in a worst case
|
|
# scenario.
|
|
# This will also be a problem in warp rando if direct access to Norman's room requires Surf or if access
|
|
# any gym leader in general requires Surf. We will probably have to force this to 0 in that case.
|
|
max_norman_count = 7
|
|
|
|
if self.options.badges == RandomizeBadges.option_vanilla:
|
|
max_norman_count = 4
|
|
|
|
if self.options.hms == RandomizeHms.option_vanilla:
|
|
if self.options.norman_requirement == NormanRequirement.option_badges:
|
|
if self.options.badges != RandomizeBadges.option_completely_random:
|
|
max_norman_count = 4
|
|
if self.options.norman_requirement == NormanRequirement.option_gyms:
|
|
max_norman_count = 4
|
|
|
|
if self.options.norman_count.value > max_norman_count:
|
|
logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with "
|
|
"other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player))
|
|
self.options.norman_count.value = max_norman_count
|
|
|
|
def create_regions(self) -> None:
|
|
regions = create_regions(self)
|
|
|
|
tags = {"Badge", "HM", "KeyItem", "Rod", "Bike"}
|
|
if self.options.overworld_items:
|
|
tags.add("OverworldItem")
|
|
if self.options.hidden_items:
|
|
tags.add("HiddenItem")
|
|
if self.options.npc_gifts:
|
|
tags.add("NpcGift")
|
|
if self.options.enable_ferry:
|
|
tags.add("Ferry")
|
|
create_locations_with_tags(self, regions, tags)
|
|
|
|
self.multiworld.regions.extend(regions.values())
|
|
|
|
# Exclude locations which are always locked behind the player's goal
|
|
def exclude_locations(location_names: List[str]):
|
|
for location_name in location_names:
|
|
try:
|
|
self.multiworld.get_location(location_name,
|
|
self.player).progress_type = LocationProgressType.EXCLUDED
|
|
except KeyError:
|
|
continue # Location not in multiworld
|
|
|
|
if self.options.goal == Goal.option_champion:
|
|
# Always required to beat champion before receiving this
|
|
exclude_locations([
|
|
"Littleroot Town - S.S. Ticket from Norman"
|
|
])
|
|
|
|
# S.S. Ticket requires beating champion, so ferry is not accessible until after goal
|
|
if not self.options.enable_ferry:
|
|
exclude_locations([
|
|
"SS Tidal - Hidden Item in Lower Deck Trash Can",
|
|
"SS Tidal - TM49 from Thief"
|
|
])
|
|
|
|
# Construction workers don't move until champion is defeated
|
|
if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value:
|
|
exclude_locations([
|
|
"Safari Zone NE - Hidden Item North",
|
|
"Safari Zone NE - Hidden Item East",
|
|
"Safari Zone NE - Item on Ledge",
|
|
"Safari Zone SE - Hidden Item in South Grass 1",
|
|
"Safari Zone SE - Hidden Item in South Grass 2",
|
|
"Safari Zone SE - Item in Grass"
|
|
])
|
|
elif self.options.goal == Goal.option_norman:
|
|
# If the player sets their options such that Surf or the Balance
|
|
# Badge is vanilla, a very large number of locations become
|
|
# "post-Norman". Similarly, access to the E4 may require you to
|
|
# defeat Norman as an event or to get his badge, making postgame
|
|
# locations inaccessible. Detecting these situations isn't trivial
|
|
# and excluding all locations requiring Surf would be a bad idea.
|
|
# So for now we just won't touch it and blame the user for
|
|
# constructing their options in this way. Players usually expect
|
|
# to only partially complete their world when playing this goal
|
|
# anyway.
|
|
|
|
# Locations which are directly unlocked by defeating Norman.
|
|
exclude_locations([
|
|
"Petalburg Gym - Balance Badge",
|
|
"Petalburg Gym - TM42 from Norman",
|
|
"Petalburg City - HM03 from Wally's Uncle",
|
|
"Dewford Town - TM36 from Sludge Bomb Man",
|
|
"Mauville City - Basement Key from Wattson",
|
|
"Mauville City - TM24 from Wattson"
|
|
])
|
|
|
|
def create_items(self) -> None:
|
|
item_locations: List[PokemonEmeraldLocation] = [
|
|
location
|
|
for location in self.multiworld.get_locations(self.player)
|
|
if location.address is not None
|
|
]
|
|
|
|
# Filter progression items which shouldn't be shuffled into the itempool. Their locations
|
|
# still exist, but event items will be placed and locked at their vanilla locations instead.
|
|
filter_tags = set()
|
|
|
|
if not self.options.key_items:
|
|
filter_tags.add("KeyItem")
|
|
if not self.options.rods:
|
|
filter_tags.add("Rod")
|
|
if not self.options.bikes:
|
|
filter_tags.add("Bike")
|
|
|
|
if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
|
|
filter_tags.add("Badge")
|
|
if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
|
|
filter_tags.add("HM")
|
|
|
|
if self.options.badges == RandomizeBadges.option_shuffle:
|
|
self.badge_shuffle_info = [
|
|
(location, self.create_item_by_code(location.default_item_code))
|
|
for location in [l for l in item_locations if "Badge" in l.tags]
|
|
]
|
|
if self.options.hms == RandomizeHms.option_shuffle:
|
|
self.hm_shuffle_info = [
|
|
(location, self.create_item_by_code(location.default_item_code))
|
|
for location in [l for l in item_locations if "HM" in l.tags]
|
|
]
|
|
|
|
item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0]
|
|
default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
|
|
|
|
if self.options.item_pool_type == ItemPoolType.option_shuffled:
|
|
self.multiworld.itempool += default_itempool
|
|
|
|
elif self.options.item_pool_type in {ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced}:
|
|
item_categories = ["Ball", "Heal", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc"]
|
|
|
|
# Count occurrences of types of vanilla items in pool
|
|
item_category_counter = Counter()
|
|
for item in default_itempool:
|
|
if not item.advancement:
|
|
item_category_counter.update([tag for tag in item.tags if tag in item_categories])
|
|
|
|
item_category_weights = [item_category_counter.get(category) for category in item_categories]
|
|
item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights]
|
|
|
|
# Create lists of item codes that can be used to fill
|
|
fill_item_candidates = emerald_data.items.values()
|
|
|
|
fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags]
|
|
|
|
fill_item_candidates_by_category = {category: [] for category in item_categories}
|
|
for item_data in fill_item_candidates:
|
|
for category in item_categories:
|
|
if category in item_data.tags:
|
|
fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id))
|
|
|
|
for category in fill_item_candidates_by_category:
|
|
fill_item_candidates_by_category[category].sort()
|
|
|
|
# Ignore vanilla occurrences and pick completely randomly
|
|
if self.options.item_pool_type == ItemPoolType.option_diverse:
|
|
item_category_weights = [
|
|
len(category_list)
|
|
for category_list in fill_item_candidates_by_category.values()
|
|
]
|
|
|
|
# TMs should not have duplicates until every TM has been used already
|
|
all_tm_choices = fill_item_candidates_by_category["TM"].copy()
|
|
|
|
def refresh_tm_choices() -> None:
|
|
fill_item_candidates_by_category["TM"] = all_tm_choices.copy()
|
|
self.random.shuffle(fill_item_candidates_by_category["TM"])
|
|
refresh_tm_choices()
|
|
|
|
# Create items
|
|
for item in default_itempool:
|
|
if not item.advancement and "Unique" not in item.tags:
|
|
category = self.random.choices(item_categories, item_category_weights)[0]
|
|
if category == "TM":
|
|
if len(fill_item_candidates_by_category["TM"]) == 0:
|
|
refresh_tm_choices()
|
|
item_code = fill_item_candidates_by_category["TM"].pop()
|
|
else:
|
|
item_code = self.random.choice(fill_item_candidates_by_category[category])
|
|
item = self.create_item_by_code(item_code)
|
|
|
|
self.multiworld.itempool.append(item)
|
|
|
|
def set_rules(self) -> None:
|
|
set_rules(self)
|
|
|
|
def generate_basic(self) -> None:
|
|
locations: List[PokemonEmeraldLocation] = self.multiworld.get_locations(self.player)
|
|
|
|
# Set our free fly location
|
|
# If not enabled, set it to Littleroot Town by default
|
|
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
|
|
if self.options.free_fly_location:
|
|
fly_location_name = self.random.choice([
|
|
"EVENT_VISITED_SLATEPORT_CITY",
|
|
"EVENT_VISITED_MAUVILLE_CITY",
|
|
"EVENT_VISITED_VERDANTURF_TOWN",
|
|
"EVENT_VISITED_FALLARBOR_TOWN",
|
|
"EVENT_VISITED_LAVARIDGE_TOWN",
|
|
"EVENT_VISITED_FORTREE_CITY",
|
|
"EVENT_VISITED_LILYCOVE_CITY",
|
|
"EVENT_VISITED_MOSSDEEP_CITY",
|
|
"EVENT_VISITED_SOOTOPOLIS_CITY",
|
|
"EVENT_VISITED_EVER_GRANDE_CITY"
|
|
])
|
|
|
|
self.free_fly_location_id = location_visited_event_to_id_map[fly_location_name]
|
|
|
|
free_fly_location_location = self.multiworld.get_location("FREE_FLY_LOCATION", self.player)
|
|
free_fly_location_location.item = None
|
|
free_fly_location_location.place_locked_item(self.create_event(fly_location_name))
|
|
|
|
# Key items which are considered in access rules but not randomized are converted to events and placed
|
|
# in their vanilla locations so that the player can have them in their inventory for logic.
|
|
def convert_unrandomized_items_to_events(tag: str) -> None:
|
|
for location in locations:
|
|
if location.tags is not None and tag in location.tags:
|
|
location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code]))
|
|
location.progress_type = LocationProgressType.DEFAULT
|
|
location.address = None
|
|
|
|
if self.options.badges == RandomizeBadges.option_vanilla:
|
|
convert_unrandomized_items_to_events("Badge")
|
|
if self.options.hms == RandomizeHms.option_vanilla:
|
|
convert_unrandomized_items_to_events("HM")
|
|
if not self.options.rods:
|
|
convert_unrandomized_items_to_events("Rod")
|
|
if not self.options.bikes:
|
|
convert_unrandomized_items_to_events("Bike")
|
|
if not self.options.key_items:
|
|
convert_unrandomized_items_to_events("KeyItem")
|
|
|
|
def pre_fill(self) -> None:
|
|
# Items which are shuffled between their own locations
|
|
if self.options.badges == RandomizeBadges.option_shuffle:
|
|
badge_locations: List[PokemonEmeraldLocation]
|
|
badge_items: List[PokemonEmeraldItem]
|
|
|
|
# Sort order makes `fill_restrictive` try to place important badges later, which
|
|
# makes it less likely to have to swap at all, and more likely for swaps to work.
|
|
# In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms,
|
|
# so Knuckle Badge deserves highest priority if Flash is logically required.
|
|
badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)]
|
|
badge_priority = {
|
|
"Knuckle Badge": 0 if (self.options.hms == RandomizeHms.option_vanilla and self.options.require_flash) else 3,
|
|
"Balance Badge": 1,
|
|
"Dynamo Badge": 1,
|
|
"Mind Badge": 2,
|
|
"Heat Badge": 2,
|
|
"Rain Badge": 3,
|
|
"Stone Badge": 4,
|
|
"Feather Badge": 5
|
|
}
|
|
badge_items.sort(key=lambda item: badge_priority.get(item.name, 0))
|
|
|
|
# Un-exclude badge locations, since we need to put progression items on them
|
|
for location in badge_locations:
|
|
location.progress_type = LocationProgressType.DEFAULT \
|
|
if location.progress_type == LocationProgressType.EXCLUDED \
|
|
else location.progress_type
|
|
|
|
collection_state = self.multiworld.get_all_state(False)
|
|
if self.hm_shuffle_info is not None:
|
|
for _, item in self.hm_shuffle_info:
|
|
collection_state.collect(item)
|
|
|
|
# In specific very constrained conditions, fill_restrictive may run
|
|
# out of swaps before it finds a valid solution if it gets unlucky.
|
|
# This is a band-aid until fill/swap can reliably find those solutions.
|
|
attempts_remaining = 2
|
|
while attempts_remaining > 0:
|
|
attempts_remaining -= 1
|
|
self.random.shuffle(badge_locations)
|
|
try:
|
|
fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items,
|
|
single_player_placement=True, lock=True, allow_excluded=True)
|
|
break
|
|
except FillError as exc:
|
|
if attempts_remaining == 0:
|
|
raise exc
|
|
|
|
logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.")
|
|
continue
|
|
|
|
if self.options.hms == RandomizeHms.option_shuffle:
|
|
hm_locations: List[PokemonEmeraldLocation]
|
|
hm_items: List[PokemonEmeraldItem]
|
|
|
|
# Sort order makes `fill_restrictive` try to place important HMs later, which
|
|
# makes it less likely to have to swap at all, and more likely for swaps to work.
|
|
# In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms,
|
|
# so Flash deserves highest priority if it's logically required.
|
|
hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)]
|
|
hm_priority = {
|
|
"HM05 Flash": 0 if (self.options.badges == RandomizeBadges.option_vanilla and self.options.require_flash) else 3,
|
|
"HM03 Surf": 1,
|
|
"HM06 Rock Smash": 1,
|
|
"HM08 Dive": 2,
|
|
"HM04 Strength": 2,
|
|
"HM07 Waterfall": 3,
|
|
"HM01 Cut": 4,
|
|
"HM02 Fly": 5
|
|
}
|
|
hm_items.sort(key=lambda item: hm_priority.get(item.name, 0))
|
|
|
|
# Un-exclude HM locations, since we need to put progression items on them
|
|
for location in hm_locations:
|
|
location.progress_type = LocationProgressType.DEFAULT \
|
|
if location.progress_type == LocationProgressType.EXCLUDED \
|
|
else location.progress_type
|
|
|
|
collection_state = self.multiworld.get_all_state(False)
|
|
|
|
# In specific very constrained conditions, fill_restrictive may run
|
|
# out of swaps before it finds a valid solution if it gets unlucky.
|
|
# This is a band-aid until fill/swap can reliably find those solutions.
|
|
attempts_remaining = 2
|
|
while attempts_remaining > 0:
|
|
attempts_remaining -= 1
|
|
self.random.shuffle(hm_locations)
|
|
try:
|
|
fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items,
|
|
single_player_placement=True, lock=True, allow_excluded=True)
|
|
break
|
|
except FillError as exc:
|
|
if attempts_remaining == 0:
|
|
raise exc
|
|
|
|
logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.")
|
|
continue
|
|
|
|
def generate_output(self, output_directory: str) -> None:
|
|
def randomize_abilities() -> None:
|
|
# Creating list of potential abilities
|
|
ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in emerald_data.abilities}
|
|
|
|
ability_blacklist_labels = {"cacophony"}
|
|
option_ability_blacklist = self.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 emerald_data.abilities if a.ability_id not in ability_blacklist]
|
|
|
|
if self.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 self.modified_species:
|
|
if species is None:
|
|
continue
|
|
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
|
|
new_abilities = (
|
|
0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist),
|
|
0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist)
|
|
)
|
|
|
|
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 += [
|
|
self.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 self.modified_species:
|
|
if species is None:
|
|
continue
|
|
|
|
old_abilities = species.abilities
|
|
new_abilities = (
|
|
0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist),
|
|
0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist)
|
|
)
|
|
|
|
species.abilities = new_abilities
|
|
|
|
def randomize_types() -> None:
|
|
if self.options.types == RandomizeTypes.option_shuffle:
|
|
type_map = list(range(18))
|
|
self.random.shuffle(type_map)
|
|
|
|
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
|
|
# So ??? will always map to itself, and 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 self.modified_species:
|
|
if species is not None:
|
|
species.types = (type_map[species.types[0]], type_map[species.types[1]])
|
|
elif self.options.types == RandomizeTypes.option_completely_random:
|
|
for species in self.modified_species:
|
|
if species is not None:
|
|
new_type_1 = get_random_type(self.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(self.random)
|
|
|
|
species.types = (new_type_1, new_type_2)
|
|
elif self.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 self.modified_species:
|
|
if species is None:
|
|
continue
|
|
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))
|
|
self.random.shuffle(type_map)
|
|
|
|
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
|
|
# So ??? will always map to itself, and 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 += [self.modified_species[evo.species_id] for evo in evolution.evolutions]
|
|
|
|
def randomize_learnsets() -> None:
|
|
type_bias = self.options.move_match_type_bias.value
|
|
normal_bias = self.options.move_normal_type_bias.value
|
|
|
|
for species in self.modified_species:
|
|
if species is None:
|
|
continue
|
|
|
|
old_learnset = species.learnset
|
|
new_learnset: List[LearnsetMove] = []
|
|
|
|
i = 0
|
|
# Replace filler MOVE_NONEs at start of list
|
|
while old_learnset[i].move_id == 0:
|
|
if self.options.level_up_moves == LevelUpMoves.option_start_with_four_moves:
|
|
new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias,
|
|
normal_bias, species.types)
|
|
else:
|
|
new_move = 0
|
|
new_learnset.append(LearnsetMove(old_learnset[i].level, new_move))
|
|
i += 1
|
|
|
|
while i < len(old_learnset):
|
|
# Guarantees the starter has a good damaging move
|
|
if i == 3:
|
|
new_move = get_random_damaging_move(self.random, {move.move_id for move in new_learnset})
|
|
else:
|
|
new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias,
|
|
normal_bias, species.types)
|
|
new_learnset.append(LearnsetMove(old_learnset[i].level, new_move))
|
|
i += 1
|
|
|
|
species.learnset = new_learnset
|
|
|
|
def randomize_tm_hm_compatibility() -> None:
|
|
for species in self.modified_species:
|
|
if species is None:
|
|
continue
|
|
|
|
combatibility_array = int_to_bool_array(species.tm_hm_compatibility)
|
|
|
|
# TMs
|
|
for i in range(0, 50):
|
|
if self.options.tm_compatibility == TmCompatibility.option_fully_compatible:
|
|
combatibility_array[i] = True
|
|
elif self.options.tm_compatibility == TmCompatibility.option_completely_random:
|
|
combatibility_array[i] = self.random.choice([True, False])
|
|
|
|
# HMs
|
|
for i in range(50, 58):
|
|
if self.options.hm_compatibility == HmCompatibility.option_fully_compatible:
|
|
combatibility_array[i] = True
|
|
elif self.options.hm_compatibility == HmCompatibility.option_completely_random:
|
|
combatibility_array[i] = self.random.choice([True, False])
|
|
|
|
species.tm_hm_compatibility = bool_array_to_int(combatibility_array)
|
|
|
|
def randomize_tm_moves() -> None:
|
|
new_moves: Set[int] = set()
|
|
|
|
for i in range(50):
|
|
new_move = get_random_move(self.random, new_moves)
|
|
new_moves.add(new_move)
|
|
self.modified_tmhm_moves[i] = new_move
|
|
|
|
def randomize_wild_encounters() -> None:
|
|
should_match_bst = self.options.wild_pokemon in {
|
|
RandomizeWildPokemon.option_match_base_stats,
|
|
RandomizeWildPokemon.option_match_base_stats_and_type
|
|
}
|
|
should_match_type = self.options.wild_pokemon in {
|
|
RandomizeWildPokemon.option_match_type,
|
|
RandomizeWildPokemon.option_match_base_stats_and_type
|
|
}
|
|
should_allow_legendaries = self.options.allow_wild_legendaries == Toggle.option_true
|
|
|
|
for map_data in self.modified_maps:
|
|
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:
|
|
species_old_to_new_map: Dict[int, int] = {}
|
|
for species_id in table.slots:
|
|
if species_id not in species_old_to_new_map:
|
|
original_species = emerald_data.species[species_id]
|
|
target_bst = sum(original_species.base_stats) if should_match_bst else None
|
|
target_type = self.random.choice(original_species.types) if should_match_type else None
|
|
|
|
species_old_to_new_map[species_id] = get_random_species(
|
|
self.random,
|
|
self.modified_species,
|
|
target_bst,
|
|
target_type,
|
|
should_allow_legendaries
|
|
).species_id
|
|
|
|
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.rom_address)
|
|
|
|
map_data.land_encounters = new_encounters[0]
|
|
map_data.water_encounters = new_encounters[1]
|
|
map_data.fishing_encounters = new_encounters[2]
|
|
|
|
def randomize_static_encounters() -> None:
|
|
if self.options.static_encounters == RandomizeStaticEncounters.option_shuffle:
|
|
shuffled_species = [encounter.species_id for encounter in emerald_data.static_encounters]
|
|
self.random.shuffle(shuffled_species)
|
|
|
|
self.modified_static_encounters = []
|
|
for i, encounter in enumerate(emerald_data.static_encounters):
|
|
self.modified_static_encounters.append(StaticEncounterData(
|
|
shuffled_species[i],
|
|
encounter.rom_address
|
|
))
|
|
else:
|
|
should_match_bst = self.options.static_encounters in {
|
|
RandomizeStaticEncounters.option_match_base_stats,
|
|
RandomizeStaticEncounters.option_match_base_stats_and_type
|
|
}
|
|
should_match_type = self.options.static_encounters in {
|
|
RandomizeStaticEncounters.option_match_type,
|
|
RandomizeStaticEncounters.option_match_base_stats_and_type
|
|
}
|
|
|
|
for encounter in emerald_data.static_encounters:
|
|
original_species = self.modified_species[encounter.species_id]
|
|
target_bst = sum(original_species.base_stats) if should_match_bst else None
|
|
target_type = self.random.choice(original_species.types) if should_match_type else None
|
|
|
|
self.modified_static_encounters.append(StaticEncounterData(
|
|
get_random_species(self.random, self.modified_species, target_bst, target_type).species_id,
|
|
encounter.rom_address
|
|
))
|
|
|
|
def randomize_opponent_parties() -> None:
|
|
should_match_bst = self.options.trainer_parties in {
|
|
RandomizeTrainerParties.option_match_base_stats,
|
|
RandomizeTrainerParties.option_match_base_stats_and_type
|
|
}
|
|
should_match_type = self.options.trainer_parties in {
|
|
RandomizeTrainerParties.option_match_type,
|
|
RandomizeTrainerParties.option_match_base_stats_and_type
|
|
}
|
|
allow_legendaries = self.options.allow_trainer_legendaries == Toggle.option_true
|
|
|
|
per_species_tmhm_moves: Dict[int, List[int]] = {}
|
|
|
|
for trainer in self.modified_trainers:
|
|
new_party = []
|
|
for pokemon in trainer.party.pokemon:
|
|
original_species = emerald_data.species[pokemon.species_id]
|
|
target_bst = sum(original_species.base_stats) if should_match_bst else None
|
|
target_type = self.random.choice(original_species.types) if should_match_type else None
|
|
|
|
new_species = get_random_species(
|
|
self.random,
|
|
self.modified_species,
|
|
target_bst,
|
|
target_type,
|
|
allow_legendaries
|
|
)
|
|
|
|
if new_species.species_id not in per_species_tmhm_moves:
|
|
per_species_tmhm_moves[new_species.species_id] = list({
|
|
self.modified_tmhm_moves[i]
|
|
for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility))
|
|
if is_compatible
|
|
})
|
|
|
|
tm_hm_movepool = per_species_tmhm_moves[new_species.species_id]
|
|
level_up_movepool = list({
|
|
move.move_id
|
|
for move in new_species.learnset
|
|
if move.move_id != 0 and move.level <= pokemon.level
|
|
})
|
|
|
|
new_moves = (
|
|
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
|
|
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
|
|
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
|
|
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool)
|
|
)
|
|
|
|
new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
|
|
|
|
trainer.party.pokemon = new_party
|
|
|
|
def randomize_starters() -> None:
|
|
match_bst = self.options.starters in {
|
|
RandomizeStarters.option_match_base_stats,
|
|
RandomizeStarters.option_match_base_stats_and_type
|
|
}
|
|
match_type = self.options.starters in {
|
|
RandomizeStarters.option_match_type,
|
|
RandomizeStarters.option_match_base_stats_and_type
|
|
}
|
|
allow_legendaries = self.options.allow_starter_legendaries == Toggle.option_true
|
|
|
|
starter_bsts = (
|
|
sum(emerald_data.species[emerald_data.starters[0]].base_stats) if match_bst else None,
|
|
sum(emerald_data.species[emerald_data.starters[1]].base_stats) if match_bst else None,
|
|
sum(emerald_data.species[emerald_data.starters[2]].base_stats) if match_bst else None
|
|
)
|
|
|
|
starter_types = (
|
|
self.random.choice(emerald_data.species[emerald_data.starters[0]].types) if match_type else None,
|
|
self.random.choice(emerald_data.species[emerald_data.starters[1]].types) if match_type else None,
|
|
self.random.choice(emerald_data.species[emerald_data.starters[2]].types) if match_type else None
|
|
)
|
|
|
|
new_starters = (
|
|
get_random_species(self.random, self.modified_species,
|
|
starter_bsts[0], starter_types[0], allow_legendaries),
|
|
get_random_species(self.random, self.modified_species,
|
|
starter_bsts[1], starter_types[1], allow_legendaries),
|
|
get_random_species(self.random, self.modified_species,
|
|
starter_bsts[2], starter_types[2], allow_legendaries)
|
|
)
|
|
|
|
egg_code = self.options.easter_egg.value
|
|
egg_check_1 = 0
|
|
egg_check_2 = 0
|
|
|
|
for i in egg_code:
|
|
egg_check_1 += ord(i)
|
|
egg_check_2 += egg_check_1 * egg_check_1
|
|
|
|
egg = 96 + egg_check_2 - (egg_check_1 * 0x077C)
|
|
if egg_check_2 == 0x14E03A and egg < 411 and egg > 0 and egg not in range(252, 277):
|
|
self.modified_starters = (egg, egg, egg)
|
|
else:
|
|
self.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
|
|
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 = self.random.choice(potential_evolutions)
|
|
|
|
for trainer_name, starter_position, is_evolved in rival_teams[i]:
|
|
trainer_data = self.modified_trainers[emerald_data.constants[trainer_name]]
|
|
trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
|
|
|
|
self.modified_species = copy.deepcopy(emerald_data.species)
|
|
self.modified_trainers = copy.deepcopy(emerald_data.trainers)
|
|
self.modified_maps = copy.deepcopy(emerald_data.maps)
|
|
self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves)
|
|
self.modified_static_encounters = copy.deepcopy(emerald_data.static_encounters)
|
|
self.modified_starters = copy.deepcopy(emerald_data.starters)
|
|
|
|
# Randomize species data
|
|
if self.options.abilities != RandomizeAbilities.option_vanilla:
|
|
randomize_abilities()
|
|
|
|
if self.options.types != RandomizeTypes.option_vanilla:
|
|
randomize_types()
|
|
|
|
if self.options.level_up_moves != LevelUpMoves.option_vanilla:
|
|
randomize_learnsets()
|
|
|
|
randomize_tm_hm_compatibility() # Options are checked within this function
|
|
|
|
min_catch_rate = min(self.options.min_catch_rate.value, 255)
|
|
for species in self.modified_species:
|
|
if species is not None:
|
|
species.catch_rate = max(species.catch_rate, min_catch_rate)
|
|
|
|
if self.options.tm_moves:
|
|
randomize_tm_moves()
|
|
|
|
# Randomize wild encounters
|
|
if self.options.wild_pokemon != RandomizeWildPokemon.option_vanilla:
|
|
randomize_wild_encounters()
|
|
|
|
# Randomize static encounters
|
|
if self.options.static_encounters != RandomizeStaticEncounters.option_vanilla:
|
|
randomize_static_encounters()
|
|
|
|
# Randomize opponents
|
|
if self.options.trainer_parties != RandomizeTrainerParties.option_vanilla:
|
|
randomize_opponent_parties()
|
|
|
|
# Randomize starters
|
|
if self.options.starters != RandomizeStarters.option_vanilla:
|
|
randomize_starters()
|
|
|
|
generate_output(self, output_directory)
|
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
|
slot_data = self.options.as_dict(
|
|
"goal",
|
|
"badges",
|
|
"hms",
|
|
"key_items",
|
|
"bikes",
|
|
"rods",
|
|
"overworld_items",
|
|
"hidden_items",
|
|
"npc_gifts",
|
|
"require_itemfinder",
|
|
"require_flash",
|
|
"enable_ferry",
|
|
"elite_four_requirement",
|
|
"elite_four_count",
|
|
"norman_requirement",
|
|
"norman_count",
|
|
"extra_boulders",
|
|
"remove_roadblocks",
|
|
"free_fly_location",
|
|
"fly_without_badge",
|
|
)
|
|
slot_data["free_fly_location_id"] = self.free_fly_location_id
|
|
return slot_data
|
|
|
|
def create_item(self, name: str) -> PokemonEmeraldItem:
|
|
return self.create_item_by_code(self.item_name_to_id[name])
|
|
|
|
def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem:
|
|
return PokemonEmeraldItem(
|
|
self.item_id_to_name[item_code],
|
|
get_item_classification(item_code),
|
|
item_code,
|
|
self.player
|
|
)
|
|
|
|
def create_event(self, name: str) -> PokemonEmeraldItem:
|
|
return PokemonEmeraldItem(
|
|
name,
|
|
ItemClassification.progression,
|
|
None,
|
|
self.player
|
|
)
|