768 lines
35 KiB
Python
768 lines
35 KiB
Python
"""
|
|
Archipelago World definition for Pokemon Emerald Version
|
|
"""
|
|
from collections import Counter
|
|
import copy
|
|
import logging
|
|
import os
|
|
import pkgutil
|
|
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union
|
|
|
|
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
|
|
from Fill import FillError, fill_restrictive
|
|
from Options import OptionError, Toggle
|
|
import settings
|
|
from worlds.AutoWorld import WebWorld, World
|
|
|
|
from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
|
|
from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data
|
|
from .groups import ITEM_GROUPS, LOCATION_GROUPS
|
|
from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value
|
|
from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category,
|
|
set_free_fly, set_legendary_cave_entrances)
|
|
from .opponents import randomize_opponent_parties
|
|
from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
|
|
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement)
|
|
from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilities, randomize_learnsets,
|
|
randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
|
|
randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
|
|
from .rom import PokemonEmeraldProcedurePatch, write_tokens
|
|
|
|
|
|
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"]
|
|
)
|
|
|
|
setup_sv = Tutorial(
|
|
"Multivärld Installations Guide",
|
|
"En guide för att kunna spela Pokémon Emerald med Archipelago.",
|
|
"Svenska",
|
|
"setup_sv.md",
|
|
"setup/sv",
|
|
["Tsukino"]
|
|
)
|
|
|
|
tutorials = [setup_en, setup_es, setup_sv]
|
|
|
|
|
|
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 = [PokemonEmeraldProcedurePatch.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
|
|
|
|
required_client_version = (0, 4, 6)
|
|
|
|
badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
|
|
hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
|
|
free_fly_location_id: int
|
|
blacklisted_moves: Set[int]
|
|
blacklisted_wilds: Set[int]
|
|
blacklisted_starters: Set[int]
|
|
blacklisted_opponent_pokemon: Set[int]
|
|
hm_requirements: Dict[str, Union[int, List[str]]]
|
|
auth: bytes
|
|
|
|
modified_species: Dict[int, SpeciesData]
|
|
modified_maps: Dict[str, MapData]
|
|
modified_tmhm_moves: List[int]
|
|
modified_legendary_encounters: List[int]
|
|
modified_starters: Tuple[int, int, int]
|
|
modified_trainers: List[TrainerData]
|
|
|
|
def __init__(self, multiworld, player):
|
|
super(PokemonEmeraldWorld, self).__init__(multiworld, player)
|
|
self.badge_shuffle_info = None
|
|
self.hm_shuffle_info = None
|
|
self.free_fly_location_id = 0
|
|
self.blacklisted_moves = set()
|
|
self.blacklisted_wilds = set()
|
|
self.blacklisted_starters = set()
|
|
self.blacklisted_opponent_pokemon = set()
|
|
self.modified_maps = copy.deepcopy(emerald_data.maps)
|
|
self.modified_species = copy.deepcopy(emerald_data.species)
|
|
self.modified_tmhm_moves = []
|
|
self.modified_starters = emerald_data.starters
|
|
self.modified_trainers = []
|
|
self.modified_legendary_encounters = []
|
|
|
|
@classmethod
|
|
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
|
from .sanity_check import validate_regions, validate_group_maps
|
|
|
|
assert validate_regions()
|
|
assert validate_group_maps()
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
return "Great Ball"
|
|
|
|
def generate_early(self) -> None:
|
|
self.hm_requirements = {
|
|
"HM01 Cut": ["Stone Badge"],
|
|
"HM02 Fly": ["Feather Badge"],
|
|
"HM03 Surf": ["Balance Badge"],
|
|
"HM04 Strength": ["Heat Badge"],
|
|
"HM05 Flash": ["Knuckle Badge"],
|
|
"HM06 Rock Smash": ["Dynamo Badge"],
|
|
"HM07 Waterfall": ["Rain Badge"],
|
|
"HM08 Dive": ["Mind Badge"],
|
|
}
|
|
if self.options.hm_requirements == HmRequirements.option_fly_without_badge:
|
|
self.hm_requirements["HM02 Fly"] = 0
|
|
|
|
self.blacklisted_moves = {emerald_data.move_labels[label] for label in self.options.move_blacklist.value}
|
|
|
|
self.blacklisted_wilds = {
|
|
get_species_id_by_label(species_name)
|
|
for species_name in self.options.wild_encounter_blacklist.value
|
|
if species_name != "_Legendaries"
|
|
}
|
|
if "_Legendaries" in self.options.wild_encounter_blacklist.value:
|
|
self.blacklisted_wilds |= LEGENDARY_POKEMON
|
|
|
|
self.blacklisted_starters = {
|
|
get_species_id_by_label(species_name)
|
|
for species_name in self.options.starter_blacklist.value
|
|
if species_name != "_Legendaries"
|
|
}
|
|
if "_Legendaries" in self.options.starter_blacklist.value:
|
|
self.blacklisted_starters |= LEGENDARY_POKEMON
|
|
|
|
self.blacklisted_opponent_pokemon = {
|
|
get_species_id_by_label(species_name)
|
|
for species_name in self.options.trainer_party_blacklist.value
|
|
if species_name != "_Legendaries"
|
|
}
|
|
if "_Legendaries" in self.options.trainer_party_blacklist.value:
|
|
self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON
|
|
|
|
# In race mode we don't patch any item location information into the ROM
|
|
if self.multiworld.is_race and not self.options.remote_items:
|
|
logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.",
|
|
self.player, self.player_name)
|
|
self.options.remote_items.value = Toggle.option_true
|
|
|
|
if self.options.goal == Goal.option_legendary_hunt:
|
|
# Prevent turning off all legendary encounters
|
|
if len(self.options.allowed_legendary_hunt_encounters.value) == 0:
|
|
raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) needs to allow at "
|
|
"least one legendary encounter when goal is legendary hunt.")
|
|
|
|
# Prevent setting the number of required legendaries higher than the number of enabled legendaries
|
|
if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value):
|
|
logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed "
|
|
"legendary encounters. Reducing to number of allowed encounters.", self.player,
|
|
self.player_name)
|
|
self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value)
|
|
|
|
# Require random wild encounters if dexsanity is enabled
|
|
if self.options.dexsanity and self.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
|
|
raise OptionError(f"Pokemon Emerald: Player {self.player} ({self.player_name}) must not leave wild "
|
|
"encounters vanilla if enabling dexsanity.")
|
|
|
|
# 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.player_name)
|
|
self.options.norman_count.value = max_norman_count
|
|
|
|
def create_regions(self) -> None:
|
|
from .regions import create_regions
|
|
all_regions = create_regions(self)
|
|
|
|
# Categories with progression items always included
|
|
categories = {
|
|
LocationCategory.BADGE,
|
|
LocationCategory.HM,
|
|
LocationCategory.KEY,
|
|
LocationCategory.ROD,
|
|
LocationCategory.BIKE,
|
|
LocationCategory.TICKET
|
|
}
|
|
if self.options.overworld_items:
|
|
categories.add(LocationCategory.OVERWORLD_ITEM)
|
|
if self.options.hidden_items:
|
|
categories.add(LocationCategory.HIDDEN_ITEM)
|
|
if self.options.npc_gifts:
|
|
categories.add(LocationCategory.GIFT)
|
|
if self.options.berry_trees:
|
|
categories.add(LocationCategory.BERRY_TREE)
|
|
if self.options.dexsanity:
|
|
categories.add(LocationCategory.POKEDEX)
|
|
if self.options.trainersanity:
|
|
categories.add(LocationCategory.TRAINER)
|
|
create_locations_by_category(self, all_regions, categories)
|
|
|
|
self.multiworld.regions.extend(all_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 these
|
|
exclude_locations([
|
|
"Littleroot Town - S.S. Ticket from Norman",
|
|
"Littleroot Town - Aurora Ticket from Norman",
|
|
"Littleroot Town - Eon Ticket from Norman",
|
|
"Littleroot Town - Mystic Ticket from Norman",
|
|
"Littleroot Town - Old Sea Map from Norman",
|
|
"Ever Grande City - Champion Wallace",
|
|
"Meteor Falls 1F - Rival Steven",
|
|
"Trick House Puzzle 8 - Item",
|
|
])
|
|
|
|
# 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",
|
|
])
|
|
|
|
# Sacred ash is on Navel Rock, which is locked behind the event tickets
|
|
if not self.options.event_tickets:
|
|
exclude_locations([
|
|
"Navel Rock Top - Hidden Item Sacred Ash",
|
|
])
|
|
elif self.options.goal == Goal.option_steven:
|
|
exclude_locations([
|
|
"Meteor Falls 1F - Rival Steven",
|
|
])
|
|
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 - Leader Norman",
|
|
"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 will still exist, but event items will be placed and
|
|
# locked at their vanilla locations instead.
|
|
filter_categories = set()
|
|
|
|
if not self.options.key_items:
|
|
filter_categories.add(LocationCategory.KEY)
|
|
if not self.options.rods:
|
|
filter_categories.add(LocationCategory.ROD)
|
|
if not self.options.bikes:
|
|
filter_categories.add(LocationCategory.BIKE)
|
|
if not self.options.event_tickets:
|
|
filter_categories.add(LocationCategory.TICKET)
|
|
|
|
if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
|
|
filter_categories.add(LocationCategory.BADGE)
|
|
if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
|
|
filter_categories.add(LocationCategory.HM)
|
|
|
|
# If Badges and HMs are set to the `shuffle` option, don't add them to
|
|
# the normal item pool, but do create their items and save them and
|
|
# their locations for use in `pre_fill` later.
|
|
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 emerald_data.locations[l.key].category == LocationCategory.BADGE]
|
|
]
|
|
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 emerald_data.locations[l.key].category == LocationCategory.HM]
|
|
]
|
|
|
|
# Filter down locations to actual items that will be filled and create
|
|
# the itempool.
|
|
item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories]
|
|
default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
|
|
|
|
# Take the itempool as-is
|
|
if self.options.item_pool_type == ItemPoolType.option_shuffled:
|
|
self.multiworld.itempool += default_itempool
|
|
|
|
# Recreate the itempool from random items
|
|
elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced):
|
|
item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone",
|
|
"Money", "TM", "Held", "Misc", "Berry"]
|
|
|
|
# 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:
|
|
from .rules import set_rules
|
|
set_rules(self)
|
|
|
|
def generate_basic(self) -> None:
|
|
# Create auth
|
|
# self.auth = self.random.randbytes(16) # Requires >=3.9
|
|
self.auth = self.random.getrandbits(16 * 8).to_bytes(16, "little")
|
|
|
|
randomize_types(self)
|
|
randomize_wild_encounters(self)
|
|
set_free_fly(self)
|
|
set_legendary_cave_entrances(self)
|
|
|
|
# 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(category: LocationCategory) -> None:
|
|
for location in self.multiworld.get_locations(self.player):
|
|
assert isinstance(location, PokemonEmeraldLocation)
|
|
if location.key is not None and emerald_data.locations[location.key].category == category:
|
|
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(LocationCategory.BADGE)
|
|
if self.options.hms == RandomizeHms.option_vanilla:
|
|
convert_unrandomized_items_to_events(LocationCategory.HM)
|
|
if not self.options.rods:
|
|
convert_unrandomized_items_to_events(LocationCategory.ROD)
|
|
if not self.options.bikes:
|
|
convert_unrandomized_items_to_events(LocationCategory.BIKE)
|
|
if not self.options.event_tickets:
|
|
convert_unrandomized_items_to_events(LocationCategory.TICKET)
|
|
if not self.options.key_items:
|
|
convert_unrandomized_items_to_events(LocationCategory.KEY)
|
|
|
|
def pre_fill(self) -> None:
|
|
# Badges and HMs that are set to shuffle need to be placed at
|
|
# their own subset of 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.
|
|
badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)]
|
|
badge_priority = {
|
|
"Knuckle Badge": 3,
|
|
"Balance Badge": 1,
|
|
"Dynamo Badge": 1,
|
|
"Mind Badge": 2,
|
|
"Heat Badge": 2,
|
|
"Rain Badge": 3,
|
|
"Stone Badge": 4,
|
|
"Feather Badge": 5,
|
|
}
|
|
# 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.
|
|
if self.options.hms == RandomizeHms.option_vanilla and \
|
|
self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
|
|
badge_priority["Knuckle Badge"] = 0
|
|
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 HM shuffle is on, HMs are not placed and not in the pool, so
|
|
# `get_all_state` did not contain them. Collect them manually for
|
|
# this fill. We know that they will be included in all state after
|
|
# this stage.
|
|
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
|
|
|
|
# Badges are guaranteed to be either placed or in the multiworld's itempool now
|
|
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.
|
|
hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)]
|
|
hm_priority = {
|
|
"HM05 Flash": 3,
|
|
"HM03 Surf": 1,
|
|
"HM06 Rock Smash": 1,
|
|
"HM08 Dive": 2,
|
|
"HM04 Strength": 2,
|
|
"HM07 Waterfall": 3,
|
|
"HM01 Cut": 4,
|
|
"HM02 Fly": 5,
|
|
}
|
|
# 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.
|
|
if self.options.badges == RandomizeBadges.option_vanilla and \
|
|
self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
|
|
hm_priority["HM05 Flash"] = 0
|
|
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:
|
|
self.modified_trainers = copy.deepcopy(emerald_data.trainers)
|
|
self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves)
|
|
self.modified_legendary_encounters = copy.deepcopy(emerald_data.legendary_encounters)
|
|
self.modified_misc_pokemon = copy.deepcopy(emerald_data.misc_pokemon)
|
|
self.modified_starters = copy.deepcopy(emerald_data.starters)
|
|
|
|
# Modify catch rate
|
|
min_catch_rate = min(self.options.min_catch_rate.value, 255)
|
|
for species in self.modified_species.values():
|
|
species.catch_rate = max(species.catch_rate, min_catch_rate)
|
|
|
|
# Modify TM moves
|
|
if self.options.tm_tutor_moves:
|
|
new_moves: Set[int] = set()
|
|
|
|
for i in range(50):
|
|
new_move = get_random_move(self.random, new_moves | self.blacklisted_moves)
|
|
new_moves.add(new_move)
|
|
self.modified_tmhm_moves[i] = new_move
|
|
|
|
randomize_abilities(self)
|
|
randomize_learnsets(self)
|
|
randomize_tm_hm_compatibility(self)
|
|
randomize_legendary_encounters(self)
|
|
randomize_misc_pokemon(self)
|
|
randomize_opponent_parties(self)
|
|
randomize_starters(self)
|
|
|
|
patch = PokemonEmeraldProcedurePatch(player=self.player, player_name=self.player_name)
|
|
patch.write_file("base_patch.bsdiff4", pkgutil.get_data(__name__, "data/base_patch.bsdiff4"))
|
|
write_tokens(self, patch)
|
|
|
|
del self.modified_trainers
|
|
del self.modified_tmhm_moves
|
|
del self.modified_legendary_encounters
|
|
del self.modified_misc_pokemon
|
|
del self.modified_starters
|
|
del self.modified_species
|
|
|
|
# Write Output
|
|
out_file_name = self.multiworld.get_out_file_name_base(self.player)
|
|
patch.write(os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}"))
|
|
|
|
def write_spoiler(self, spoiler_handle: TextIO):
|
|
if self.options.dexsanity:
|
|
from collections import defaultdict
|
|
|
|
spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")
|
|
|
|
slot_to_rod_suffix = {
|
|
0: " (Old Rod)",
|
|
1: " (Old Rod)",
|
|
2: " (Good Rod)",
|
|
3: " (Good Rod)",
|
|
4: " (Good Rod)",
|
|
5: " (Super Rod)",
|
|
6: " (Super Rod)",
|
|
7: " (Super Rod)",
|
|
8: " (Super Rod)",
|
|
9: " (Super Rod)",
|
|
}
|
|
|
|
species_maps = defaultdict(set)
|
|
for map in self.modified_maps.values():
|
|
if map.land_encounters is not None:
|
|
for encounter in map.land_encounters.slots:
|
|
species_maps[encounter].add(map.label + " (Land)")
|
|
|
|
if map.water_encounters is not None:
|
|
for encounter in map.water_encounters.slots:
|
|
species_maps[encounter].add(map.label + " (Water)")
|
|
|
|
if map.fishing_encounters is not None:
|
|
for slot, encounter in enumerate(map.fishing_encounters.slots):
|
|
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
|
|
|
|
lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n"
|
|
for species, maps in species_maps.items()]
|
|
lines.sort()
|
|
for line in lines:
|
|
spoiler_handle.write(line)
|
|
|
|
del self.modified_maps
|
|
|
|
def extend_hint_information(self, hint_data):
|
|
if self.options.dexsanity:
|
|
from collections import defaultdict
|
|
|
|
slot_to_rod_suffix = {
|
|
0: " (Old Rod)",
|
|
1: " (Old Rod)",
|
|
2: " (Good Rod)",
|
|
3: " (Good Rod)",
|
|
4: " (Good Rod)",
|
|
5: " (Super Rod)",
|
|
6: " (Super Rod)",
|
|
7: " (Super Rod)",
|
|
8: " (Super Rod)",
|
|
9: " (Super Rod)",
|
|
}
|
|
|
|
species_maps = defaultdict(set)
|
|
for map in self.modified_maps.values():
|
|
if map.land_encounters is not None:
|
|
for encounter in map.land_encounters.slots:
|
|
species_maps[encounter].add(map.label + " (Land)")
|
|
|
|
if map.water_encounters is not None:
|
|
for encounter in map.water_encounters.slots:
|
|
species_maps[encounter].add(map.label + " (Water)")
|
|
|
|
if map.fishing_encounters is not None:
|
|
for slot, encounter in enumerate(map.fishing_encounters.slots):
|
|
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
|
|
|
|
hint_data[self.player] = {
|
|
self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps))
|
|
for species, maps in species_maps.items()
|
|
}
|
|
|
|
def modify_multidata(self, multidata: Dict[str, Any]):
|
|
import base64
|
|
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = multidata["connect_names"][self.player_name]
|
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
|
slot_data = self.options.as_dict(
|
|
"goal",
|
|
"badges",
|
|
"hms",
|
|
"key_items",
|
|
"bikes",
|
|
"event_tickets",
|
|
"rods",
|
|
"overworld_items",
|
|
"hidden_items",
|
|
"npc_gifts",
|
|
"berry_trees",
|
|
"require_itemfinder",
|
|
"require_flash",
|
|
"elite_four_requirement",
|
|
"elite_four_count",
|
|
"norman_requirement",
|
|
"norman_count",
|
|
"legendary_hunt_catch",
|
|
"legendary_hunt_count",
|
|
"extra_boulders",
|
|
"remove_roadblocks",
|
|
"allowed_legendary_hunt_encounters",
|
|
"extra_bumpy_slope",
|
|
"free_fly_location",
|
|
"remote_items",
|
|
"dexsanity",
|
|
"trainersanity",
|
|
"modify_118",
|
|
"death_link",
|
|
"normalize_encounter_rates",
|
|
)
|
|
slot_data["free_fly_location_id"] = self.free_fly_location_id
|
|
slot_data["hm_requirements"] = self.hm_requirements
|
|
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
|
|
)
|