Pokemon Emerald: Implement New Game (#1813)
This commit is contained in:
parent
e670ca513b
commit
43041f7292
|
@ -9,6 +9,7 @@
|
|||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
|
|
|
@ -52,6 +52,7 @@ Currently, the following games are supported:
|
|||
* DOOM 1993
|
||||
* Terraria
|
||||
* Lingo
|
||||
* Pokémon Emerald
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
|
@ -95,6 +95,9 @@
|
|||
# Overcooked! 2
|
||||
/worlds/overcooked2/ @toasterparty
|
||||
|
||||
# Pokemon Emerald
|
||||
/worlds/pokemon_emerald/ @Zunawe
|
||||
|
||||
# Pokemon Red and Blue
|
||||
/worlds/pokemon_rb/ @Alchav
|
||||
|
||||
|
|
|
@ -153,6 +153,11 @@ Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Arc
|
|||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/bizhawk
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/bizhawk
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Components: client/bizhawk
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/bizhawk
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2023 Zunawe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,58 @@
|
|||
# Pokemon Emerald
|
||||
|
||||
Version 1.2.0
|
||||
|
||||
This README contains general info useful for understanding the world. Pretty much all the long lists of locations,
|
||||
regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check
|
||||
[data/README.md](data/README.md) for more detailed information on the JSON files holding most of the data.
|
||||
|
||||
## Warps
|
||||
|
||||
Quick note to start, you should not be defining or modifying encoded warps from this repository. They're encoded in the
|
||||
source code repository for the mod, and then assigned to regions in `data/regions/`. All warps in the game already exist
|
||||
within `extracted_data.json`, and all relevant warps are already placed in `data/regions/` (unless they were deleted
|
||||
accidentally).
|
||||
|
||||
Many warps are actually two or three events acting as one logical warp. Doorways, for example, are often 2 tiles wide
|
||||
indoors but only 1 tile wide outdoors. Both indoor warps point to the outdoor warp, and the outdoor warp points to only
|
||||
one of the indoor warps. We want to describe warps logically in a way that retains information about individual warp
|
||||
events. That way a 2-tile-wide doorway doesnt look like a one-way warp next to an unrelated two-way warp, but if we want
|
||||
to randomize the destinations of those warps, we can still get back each individual id of the multi-tile warp.
|
||||
|
||||
This is how warps are encoded:
|
||||
|
||||
`{source_map}:{source_warp_ids}/{dest_map}:{dest_warp_ids}[!]`
|
||||
|
||||
- `source_map`: The map the warp events are located in
|
||||
- `source_warp_ids`: The ids of all adjacent warp events in source_map which lead to the same destination (these must be
|
||||
in ascending order)
|
||||
- `dest_map`: The map of the warp event to which this one is connected
|
||||
- `dest_warp_ids`: The ids of the warp events in dest_map
|
||||
- `[!]`: If the warp expects to lead to a destination which doesnot lead back to it, add a ! to the end
|
||||
|
||||
Example: `MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4`
|
||||
|
||||
Example 2: `MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!`
|
||||
|
||||
Note: A warp must have its destination set to another warp event. However, that does not guarantee that the destination
|
||||
warp event will warp back to the source.
|
||||
|
||||
Note 2: Some warps _only_ act as destinations and cannot actually be interacted with by the player as sources. These are
|
||||
usually places you fall from a hole above. At the time of writing, these are actually not accounted for, but there are
|
||||
no instances where it changes logical access.
|
||||
|
||||
Note 3: Some warp destinations go to the map `MAP_DYNAMIC` and have a special warp id. These edge cases are:
|
||||
|
||||
- The Moving Truck
|
||||
- Terra Cave
|
||||
- Marine Cave
|
||||
- The Department Store Elevator
|
||||
- Secret Bases
|
||||
- The Trade Center
|
||||
- The Union Room
|
||||
- The Record Corner
|
||||
- 2P/4P Battle Colosseum
|
||||
|
||||
Note 4: The trick house on Route 110 changes the warp destinations of its entrance and ending room as you progress
|
||||
through the puzzles, but the source code only sets the trick house up for the first puzzle, and I assume the destination
|
||||
gets overwritten at run time when certain flags are set.
|
|
@ -0,0 +1,882 @@
|
|||
"""
|
||||
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
|
||||
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 (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"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
|
||||
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())
|
||||
|
||||
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"])
|
||||
|
||||
# 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.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))
|
||||
|
||||
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))
|
||||
|
||||
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.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
|
||||
)
|
|
@ -0,0 +1,277 @@
|
|||
from typing import TYPE_CHECKING, Dict, Set
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
import worlds._bizhawk as bizhawk
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
from .data import BASE_OFFSET, data
|
||||
from .options import Goal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
|
||||
|
||||
EXPECTED_ROM_NAME = "pokemon emerald version / AP 2"
|
||||
|
||||
IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"]
|
||||
DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"]
|
||||
DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"]
|
||||
|
||||
# These flags are communicated to the tracker as a bitfield using this order.
|
||||
# Modifying the order will cause undetectable autotracking issues.
|
||||
TRACKER_EVENT_FLAGS = [
|
||||
"FLAG_DEFEATED_RUSTBORO_GYM",
|
||||
"FLAG_DEFEATED_DEWFORD_GYM",
|
||||
"FLAG_DEFEATED_MAUVILLE_GYM",
|
||||
"FLAG_DEFEATED_LAVARIDGE_GYM",
|
||||
"FLAG_DEFEATED_PETALBURG_GYM",
|
||||
"FLAG_DEFEATED_FORTREE_GYM",
|
||||
"FLAG_DEFEATED_MOSSDEEP_GYM",
|
||||
"FLAG_DEFEATED_SOOTOPOLIS_GYM",
|
||||
"FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone
|
||||
"FLAG_DELIVERED_STEVEN_LETTER",
|
||||
"FLAG_DELIVERED_DEVON_GOODS",
|
||||
"FLAG_HIDE_ROUTE_119_TEAM_AQUA", # Clear Weather Institute
|
||||
"FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite
|
||||
"FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout
|
||||
"FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine
|
||||
"FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE", # Clear Aqua Hideout
|
||||
"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE", # Clear Space Center
|
||||
"FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN",
|
||||
"FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis
|
||||
"FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt)
|
||||
"FLAG_IS_CHAMPION",
|
||||
"FLAG_PURCHASED_HARBOR_MAIL"
|
||||
]
|
||||
EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS}
|
||||
|
||||
KEY_LOCATION_FLAGS = [
|
||||
"NPC_GIFT_RECEIVED_HM01",
|
||||
"NPC_GIFT_RECEIVED_HM02",
|
||||
"NPC_GIFT_RECEIVED_HM03",
|
||||
"NPC_GIFT_RECEIVED_HM04",
|
||||
"NPC_GIFT_RECEIVED_HM05",
|
||||
"NPC_GIFT_RECEIVED_HM06",
|
||||
"NPC_GIFT_RECEIVED_HM07",
|
||||
"NPC_GIFT_RECEIVED_HM08",
|
||||
"NPC_GIFT_RECEIVED_ACRO_BIKE",
|
||||
"NPC_GIFT_RECEIVED_WAILMER_PAIL",
|
||||
"NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
|
||||
"NPC_GIFT_RECEIVED_LETTER",
|
||||
"NPC_GIFT_RECEIVED_METEORITE",
|
||||
"NPC_GIFT_RECEIVED_GO_GOGGLES",
|
||||
"NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON",
|
||||
"NPC_GIFT_RECEIVED_ITEMFINDER",
|
||||
"NPC_GIFT_RECEIVED_DEVON_SCOPE",
|
||||
"NPC_GIFT_RECEIVED_MAGMA_EMBLEM",
|
||||
"NPC_GIFT_RECEIVED_POKEBLOCK_CASE",
|
||||
"NPC_GIFT_RECEIVED_SS_TICKET",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY",
|
||||
"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_4_SCANNER",
|
||||
"ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY",
|
||||
"NPC_GIFT_RECEIVED_OLD_ROD",
|
||||
"NPC_GIFT_RECEIVED_GOOD_ROD",
|
||||
"NPC_GIFT_RECEIVED_SUPER_ROD",
|
||||
]
|
||||
KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}
|
||||
|
||||
|
||||
class PokemonEmeraldClient(BizHawkClient):
|
||||
game = "Pokemon Emerald"
|
||||
system = "GBA"
|
||||
patch_suffix = ".apemerald"
|
||||
local_checked_locations: Set[int]
|
||||
local_set_events: Dict[str, bool]
|
||||
local_found_key_items: Dict[str, bool]
|
||||
goal_flag: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.local_checked_locations = set()
|
||||
self.local_set_events = {}
|
||||
self.local_found_key_items = {}
|
||||
self.goal_flag = IS_CHAMPION_FLAG
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
|
||||
try:
|
||||
# Check ROM name/patch version
|
||||
rom_name_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x108, 32, "ROM")]))[0])
|
||||
rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode("ascii")
|
||||
if not rom_name.startswith("pokemon emerald version"):
|
||||
return False
|
||||
if rom_name == "pokemon emerald version":
|
||||
logger.info("ERROR: You appear to be running an unpatched version of Pokemon Emerald. "
|
||||
"You need to generate a patch file and use it to create a patched ROM.")
|
||||
return False
|
||||
if rom_name != EXPECTED_ROM_NAME:
|
||||
logger.info("ERROR: The patch file used to create this ROM is not compatible with "
|
||||
"this client. Double check your client version against the version being "
|
||||
"used by the generator.")
|
||||
return False
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except bizhawk.RequestFailedError:
|
||||
return False # Should verify on the next pass
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001
|
||||
ctx.want_slot_data = True
|
||||
ctx.watcher_timeout = 0.125
|
||||
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 64, "ROM")]))[0]
|
||||
ctx.auth = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8")
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
if ctx.slot_data is not None:
|
||||
if ctx.slot_data["goal"] == Goal.option_champion:
|
||||
self.goal_flag = IS_CHAMPION_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_steven:
|
||||
self.goal_flag = DEFEATED_STEVEN_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_norman:
|
||||
self.goal_flag = DEFEATED_NORMAN_FLAG
|
||||
|
||||
try:
|
||||
# Checks that the player is in the overworld
|
||||
overworld_guard = (data.ram_addresses["gMain"] + 4, (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"), "System Bus")
|
||||
|
||||
# Read save block address
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus")],
|
||||
[overworld_guard]
|
||||
)
|
||||
if read_result is None: # Not in overworld
|
||||
return
|
||||
|
||||
# Checks that the save block hasn't moved
|
||||
save_block_address_guard = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
|
||||
|
||||
save_block_address = int.from_bytes(read_result[0], "little")
|
||||
|
||||
# Handle giving the player items
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[
|
||||
(save_block_address + 0x3778, 2, "System Bus"), # Number of received items
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, 1, "System Bus") # Received item struct full?
|
||||
],
|
||||
[overworld_guard, save_block_address_guard]
|
||||
)
|
||||
if read_result is None: # Not in overworld, or save block moved
|
||||
return
|
||||
|
||||
num_received_items = int.from_bytes(read_result[0], "little")
|
||||
received_item_is_empty = read_result[1][0] == 0
|
||||
|
||||
# If the game hasn't received all items yet and the received item struct doesn't contain an item, then
|
||||
# fill it with the next item
|
||||
if num_received_items < len(ctx.items_received) and received_item_is_empty:
|
||||
next_item = ctx.items_received[num_received_items]
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, [1], "System Bus"), # Mark struct full
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 5, [next_item.flags & 1], "System Bus"),
|
||||
])
|
||||
|
||||
# Read flags in 2 chunks
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(save_block_address + 0x1450, 0x96, "System Bus")], # Flags
|
||||
[overworld_guard, save_block_address_guard]
|
||||
)
|
||||
if read_result is None: # Not in overworld, or save block moved
|
||||
return
|
||||
|
||||
flag_bytes = read_result[0]
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(save_block_address + 0x14E6, 0x96, "System Bus")], # Flags
|
||||
[overworld_guard, save_block_address_guard]
|
||||
)
|
||||
if read_result is not None:
|
||||
flag_bytes += read_result[0]
|
||||
|
||||
game_clear = False
|
||||
local_checked_locations = set()
|
||||
local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS}
|
||||
local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS}
|
||||
|
||||
# Check set flags
|
||||
for byte_i, byte in enumerate(flag_bytes):
|
||||
for i in range(8):
|
||||
if byte & (1 << i) != 0:
|
||||
flag_id = byte_i * 8 + i
|
||||
|
||||
location_id = flag_id + BASE_OFFSET
|
||||
if location_id in ctx.server_locations:
|
||||
local_checked_locations.add(location_id)
|
||||
|
||||
if flag_id == self.goal_flag:
|
||||
game_clear = True
|
||||
|
||||
if flag_id in EVENT_FLAG_MAP:
|
||||
local_set_events[EVENT_FLAG_MAP[flag_id]] = True
|
||||
|
||||
if flag_id in KEY_LOCATION_FLAG_MAP:
|
||||
local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True
|
||||
|
||||
# Send locations
|
||||
if local_checked_locations != self.local_checked_locations:
|
||||
self.local_checked_locations = local_checked_locations
|
||||
|
||||
if local_checked_locations is not None:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationChecks",
|
||||
"locations": list(local_checked_locations)
|
||||
}])
|
||||
|
||||
# Send game clear
|
||||
if not ctx.finished_game and game_clear:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
|
||||
# Send tracker event flags
|
||||
if local_set_events != self.local_set_events and ctx.slot is not None:
|
||||
event_bitfield = 0
|
||||
for i, flag_name in enumerate(TRACKER_EVENT_FLAGS):
|
||||
if local_set_events[flag_name]:
|
||||
event_bitfield |= 1 << i
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "replace", "value": event_bitfield}]
|
||||
}])
|
||||
self.local_set_events = local_set_events
|
||||
|
||||
if local_found_key_items != self.local_found_key_items:
|
||||
key_bitfield = 0
|
||||
for i, location_name in enumerate(KEY_LOCATION_FLAGS):
|
||||
if local_found_key_items[location_name]:
|
||||
key_bitfield |= 1 << i
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "replace", "value": key_bitfield}]
|
||||
}])
|
||||
self.local_found_key_items = local_found_key_items
|
||||
except bizhawk.RequestFailedError:
|
||||
# Exit handler and return to main loop to reconnect
|
||||
pass
|
|
@ -0,0 +1,995 @@
|
|||
"""
|
||||
Pulls data from JSON files in worlds/pokemon_emerald/data/ into classes.
|
||||
This also includes marrying automatically extracted data with manually
|
||||
defined data (like location labels or usable pokemon species), some cleanup
|
||||
and sorting, and Warp methods.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import copy
|
||||
from enum import IntEnum
|
||||
import orjson
|
||||
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
|
||||
import pkgutil
|
||||
import pkg_resources
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
|
||||
BASE_OFFSET = 3860000
|
||||
|
||||
|
||||
class Warp:
|
||||
"""
|
||||
Represents warp events in the game like doorways or warp pads
|
||||
"""
|
||||
is_one_way: bool
|
||||
source_map: str
|
||||
source_ids: List[int]
|
||||
dest_map: str
|
||||
dest_ids: List[int]
|
||||
parent_region: Optional[str]
|
||||
|
||||
def __init__(self, encoded_string: Optional[str] = None, parent_region: Optional[str] = None) -> None:
|
||||
if encoded_string is not None:
|
||||
decoded_warp = Warp.decode(encoded_string)
|
||||
self.is_one_way = decoded_warp.is_one_way
|
||||
self.source_map = decoded_warp.source_map
|
||||
self.source_ids = decoded_warp.source_ids
|
||||
self.dest_map = decoded_warp.dest_map
|
||||
self.dest_ids = decoded_warp.dest_ids
|
||||
self.parent_region = parent_region
|
||||
|
||||
def encode(self) -> str:
|
||||
"""
|
||||
Returns a string encoding of this warp
|
||||
"""
|
||||
source_ids_string = ""
|
||||
for source_id in self.source_ids:
|
||||
source_ids_string += str(source_id) + ","
|
||||
source_ids_string = source_ids_string[:-1] # Remove last ","
|
||||
|
||||
dest_ids_string = ""
|
||||
for dest_id in self.dest_ids:
|
||||
dest_ids_string += str(dest_id) + ","
|
||||
dest_ids_string = dest_ids_string[:-1] # Remove last ","
|
||||
|
||||
return f"{self.source_map}:{source_ids_string}/{self.dest_map}:{dest_ids_string}{'!' if self.is_one_way else ''}"
|
||||
|
||||
def connects_to(self, other: 'Warp') -> bool:
|
||||
"""
|
||||
Returns true if this warp sends the player to `other`
|
||||
"""
|
||||
return self.dest_map == other.source_map and set(self.dest_ids) <= set(other.source_ids)
|
||||
|
||||
@staticmethod
|
||||
def decode(encoded_string: str) -> 'Warp':
|
||||
"""
|
||||
Create a Warp object from an encoded string
|
||||
"""
|
||||
warp = Warp()
|
||||
warp.is_one_way = encoded_string.endswith("!")
|
||||
if warp.is_one_way:
|
||||
encoded_string = encoded_string[:-1]
|
||||
|
||||
warp_source, warp_dest = encoded_string.split("/")
|
||||
warp_source_map, warp_source_indices = warp_source.split(":")
|
||||
warp_dest_map, warp_dest_indices = warp_dest.split(":")
|
||||
|
||||
warp.source_map = warp_source_map
|
||||
warp.dest_map = warp_dest_map
|
||||
|
||||
warp.source_ids = [int(index) for index in warp_source_indices.split(",")]
|
||||
warp.dest_ids = [int(index) for index in warp_dest_indices.split(",")]
|
||||
|
||||
return warp
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
label: str
|
||||
item_id: int
|
||||
classification: ItemClassification
|
||||
tags: FrozenSet[str]
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
name: str
|
||||
label: str
|
||||
parent_region: str
|
||||
default_item: int
|
||||
rom_address: int
|
||||
flag: int
|
||||
tags: FrozenSet[str]
|
||||
|
||||
|
||||
class EventData(NamedTuple):
|
||||
name: str
|
||||
parent_region: str
|
||||
|
||||
|
||||
class RegionData:
|
||||
name: str
|
||||
exits: List[str]
|
||||
warps: List[str]
|
||||
locations: List[str]
|
||||
events: List[EventData]
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.exits = []
|
||||
self.warps = []
|
||||
self.locations = []
|
||||
self.events = []
|
||||
|
||||
|
||||
class BaseStats(NamedTuple):
|
||||
hp: int
|
||||
attack: int
|
||||
defense: int
|
||||
speed: int
|
||||
special_attack: int
|
||||
special_defense: int
|
||||
|
||||
|
||||
class LearnsetMove(NamedTuple):
|
||||
level: int
|
||||
move_id: int
|
||||
|
||||
|
||||
class EvolutionMethodEnum(IntEnum):
|
||||
LEVEL = 0
|
||||
LEVEL_ATK_LT_DEF = 1
|
||||
LEVEL_ATK_EQ_DEF = 2
|
||||
LEVEL_ATK_GT_DEF = 3
|
||||
LEVEL_SILCOON = 4
|
||||
LEVEL_CASCOON = 5
|
||||
LEVEL_NINJASK = 6
|
||||
LEVEL_SHEDINJA = 7
|
||||
ITEM = 8
|
||||
FRIENDSHIP = 9
|
||||
FRIENDSHIP_DAY = 10
|
||||
FRIENDSHIP_NIGHT = 11
|
||||
|
||||
|
||||
def _str_to_evolution_method(string: str) -> EvolutionMethodEnum:
|
||||
if string == "LEVEL":
|
||||
return EvolutionMethodEnum.LEVEL
|
||||
if string == "LEVEL_ATK_LT_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_LT_DEF
|
||||
if string == "LEVEL_ATK_EQ_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF
|
||||
if string == "LEVEL_ATK_GT_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_GT_DEF
|
||||
if string == "LEVEL_SILCOON":
|
||||
return EvolutionMethodEnum.LEVEL_SILCOON
|
||||
if string == "LEVEL_CASCOON":
|
||||
return EvolutionMethodEnum.LEVEL_CASCOON
|
||||
if string == "LEVEL_NINJASK":
|
||||
return EvolutionMethodEnum.LEVEL_NINJASK
|
||||
if string == "LEVEL_SHEDINJA":
|
||||
return EvolutionMethodEnum.LEVEL_SHEDINJA
|
||||
if string == "FRIENDSHIP":
|
||||
return EvolutionMethodEnum.FRIENDSHIP
|
||||
if string == "FRIENDSHIP_DAY":
|
||||
return EvolutionMethodEnum.FRIENDSHIP_DAY
|
||||
if string == "FRIENDSHIP_NIGHT":
|
||||
return EvolutionMethodEnum.FRIENDSHIP_NIGHT
|
||||
|
||||
|
||||
class EvolutionData(NamedTuple):
|
||||
method: EvolutionMethodEnum
|
||||
param: int
|
||||
species_id: int
|
||||
|
||||
|
||||
class StaticEncounterData(NamedTuple):
|
||||
species_id: int
|
||||
rom_address: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpeciesData:
|
||||
name: str
|
||||
label: str
|
||||
species_id: int
|
||||
base_stats: BaseStats
|
||||
types: Tuple[int, int]
|
||||
abilities: Tuple[int, int]
|
||||
evolutions: List[EvolutionData]
|
||||
pre_evolution: Optional[int]
|
||||
catch_rate: int
|
||||
learnset: List[LearnsetMove]
|
||||
tm_hm_compatibility: int
|
||||
learnset_rom_address: int
|
||||
rom_address: int
|
||||
|
||||
|
||||
class AbilityData(NamedTuple):
|
||||
ability_id: int
|
||||
label: str
|
||||
|
||||
|
||||
class EncounterTableData(NamedTuple):
|
||||
slots: List[int]
|
||||
rom_address: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class MapData:
|
||||
name: str
|
||||
land_encounters: Optional[EncounterTableData]
|
||||
water_encounters: Optional[EncounterTableData]
|
||||
fishing_encounters: Optional[EncounterTableData]
|
||||
|
||||
|
||||
class TrainerPokemonDataTypeEnum(IntEnum):
|
||||
NO_ITEM_DEFAULT_MOVES = 0
|
||||
ITEM_DEFAULT_MOVES = 1
|
||||
NO_ITEM_CUSTOM_MOVES = 2
|
||||
ITEM_CUSTOM_MOVES = 3
|
||||
|
||||
|
||||
def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum:
|
||||
if string == "NO_ITEM_DEFAULT_MOVES":
|
||||
return TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES
|
||||
if string == "ITEM_DEFAULT_MOVES":
|
||||
return TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES
|
||||
if string == "NO_ITEM_CUSTOM_MOVES":
|
||||
return TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES
|
||||
if string == "ITEM_CUSTOM_MOVES":
|
||||
return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainerPokemonData:
|
||||
species_id: int
|
||||
level: int
|
||||
moves: Optional[Tuple[int, int, int, int]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainerPartyData:
|
||||
pokemon: List[TrainerPokemonData]
|
||||
pokemon_data_type: TrainerPokemonDataTypeEnum
|
||||
rom_address: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainerData:
|
||||
trainer_id: int
|
||||
party: TrainerPartyData
|
||||
rom_address: int
|
||||
battle_script_rom_address: int
|
||||
|
||||
|
||||
class PokemonEmeraldData:
|
||||
starters: Tuple[int, int, int]
|
||||
constants: Dict[str, int]
|
||||
ram_addresses: Dict[str, int]
|
||||
rom_addresses: Dict[str, int]
|
||||
regions: Dict[str, RegionData]
|
||||
locations: Dict[str, LocationData]
|
||||
items: Dict[int, ItemData]
|
||||
species: List[Optional[SpeciesData]]
|
||||
static_encounters: List[StaticEncounterData]
|
||||
tmhm_moves: List[int]
|
||||
abilities: List[AbilityData]
|
||||
maps: List[MapData]
|
||||
warps: Dict[str, Warp]
|
||||
warp_map: Dict[str, Optional[str]]
|
||||
trainers: List[TrainerData]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.starters = (277, 280, 283)
|
||||
self.constants = {}
|
||||
self.ram_addresses = {}
|
||||
self.rom_addresses = {}
|
||||
self.regions = {}
|
||||
self.locations = {}
|
||||
self.items = {}
|
||||
self.species = []
|
||||
self.static_encounters = []
|
||||
self.tmhm_moves = []
|
||||
self.abilities = []
|
||||
self.maps = []
|
||||
self.warps = {}
|
||||
self.warp_map = {}
|
||||
self.trainers = []
|
||||
|
||||
|
||||
def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]:
|
||||
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode('utf-8-sig'))
|
||||
|
||||
|
||||
data = PokemonEmeraldData()
|
||||
|
||||
def create_data_copy() -> PokemonEmeraldData:
|
||||
new_copy = PokemonEmeraldData()
|
||||
new_copy.species = copy.deepcopy(data.species)
|
||||
new_copy.tmhm_moves = copy.deepcopy(data.tmhm_moves)
|
||||
new_copy.maps = copy.deepcopy(data.maps)
|
||||
new_copy.static_encounters = copy.deepcopy(data.static_encounters)
|
||||
new_copy.trainers = copy.deepcopy(data.trainers)
|
||||
|
||||
|
||||
def _init() -> None:
|
||||
extracted_data: Dict[str, Any] = load_json_data("extracted_data.json")
|
||||
data.constants = extracted_data["constants"]
|
||||
data.ram_addresses = extracted_data["misc_ram_addresses"]
|
||||
data.rom_addresses = extracted_data["misc_rom_addresses"]
|
||||
|
||||
location_attributes_json = load_json_data("locations.json")
|
||||
|
||||
# Load/merge region json files
|
||||
region_json_list = []
|
||||
for file in pkg_resources.resource_listdir(__name__, "data/regions"):
|
||||
if not pkg_resources.resource_isdir(__name__, "data/regions/" + file):
|
||||
region_json_list.append(load_json_data("regions/" + file))
|
||||
|
||||
regions_json = {}
|
||||
for region_subset in region_json_list:
|
||||
for region_name, region_json in region_subset.items():
|
||||
if region_name in regions_json:
|
||||
raise AssertionError("Region [{region_name}] was defined multiple times")
|
||||
regions_json[region_name] = region_json
|
||||
|
||||
# Create region data
|
||||
claimed_locations: Set[str] = set()
|
||||
claimed_warps: Set[str] = set()
|
||||
|
||||
data.regions = {}
|
||||
for region_name, region_json in regions_json.items():
|
||||
new_region = RegionData(region_name)
|
||||
|
||||
# Locations
|
||||
for location_name in region_json["locations"]:
|
||||
if location_name in claimed_locations:
|
||||
raise AssertionError(f"Location [{location_name}] was claimed by multiple regions")
|
||||
|
||||
location_json = extracted_data["locations"][location_name]
|
||||
new_location = LocationData(
|
||||
location_name,
|
||||
location_attributes_json[location_name]["label"],
|
||||
region_name,
|
||||
location_json["default_item"],
|
||||
location_json["rom_address"],
|
||||
location_json["flag"],
|
||||
frozenset(location_attributes_json[location_name]["tags"])
|
||||
)
|
||||
new_region.locations.append(location_name)
|
||||
data.locations[location_name] = new_location
|
||||
claimed_locations.add(location_name)
|
||||
|
||||
new_region.locations.sort()
|
||||
|
||||
# Events
|
||||
for event in region_json["events"]:
|
||||
new_region.events.append(EventData(event, region_name))
|
||||
|
||||
# Exits
|
||||
for region_exit in region_json["exits"]:
|
||||
new_region.exits.append(region_exit)
|
||||
|
||||
# Warps
|
||||
for encoded_warp in region_json["warps"]:
|
||||
if encoded_warp in claimed_warps:
|
||||
raise AssertionError(f"Warp [{encoded_warp}] was claimed by multiple regions")
|
||||
new_region.warps.append(encoded_warp)
|
||||
data.warps[encoded_warp] = Warp(encoded_warp, region_name)
|
||||
claimed_warps.add(encoded_warp)
|
||||
|
||||
new_region.warps.sort()
|
||||
|
||||
data.regions[region_name] = new_region
|
||||
|
||||
# Create item data
|
||||
items_json = load_json_data("items.json")
|
||||
|
||||
data.items = {}
|
||||
for item_constant_name, attributes in items_json.items():
|
||||
item_classification = None
|
||||
if attributes["classification"] == "PROGRESSION":
|
||||
item_classification = ItemClassification.progression
|
||||
elif attributes["classification"] == "USEFUL":
|
||||
item_classification = ItemClassification.useful
|
||||
elif attributes["classification"] == "FILLER":
|
||||
item_classification = ItemClassification.filler
|
||||
elif attributes["classification"] == "TRAP":
|
||||
item_classification = ItemClassification.trap
|
||||
else:
|
||||
raise ValueError(f"Unknown classification {attributes['classification']} for item {item_constant_name}")
|
||||
|
||||
data.items[data.constants[item_constant_name]] = ItemData(
|
||||
attributes["label"],
|
||||
data.constants[item_constant_name],
|
||||
item_classification,
|
||||
frozenset(attributes["tags"])
|
||||
)
|
||||
|
||||
# Create species data
|
||||
|
||||
# Excludes extras like copies of Unown and special species values like SPECIES_EGG.
|
||||
all_species: List[Tuple[str, str]] = [
|
||||
("SPECIES_BULBASAUR", "Bulbasaur"),
|
||||
("SPECIES_IVYSAUR", "Ivysaur"),
|
||||
("SPECIES_VENUSAUR", "Venusaur"),
|
||||
("SPECIES_CHARMANDER", "Charmander"),
|
||||
("SPECIES_CHARMELEON", "Charmeleon"),
|
||||
("SPECIES_CHARIZARD", "Charizard"),
|
||||
("SPECIES_SQUIRTLE", "Squirtle"),
|
||||
("SPECIES_WARTORTLE", "Wartortle"),
|
||||
("SPECIES_BLASTOISE", "Blastoise"),
|
||||
("SPECIES_CATERPIE", "Caterpie"),
|
||||
("SPECIES_METAPOD", "Metapod"),
|
||||
("SPECIES_BUTTERFREE", "Butterfree"),
|
||||
("SPECIES_WEEDLE", "Weedle"),
|
||||
("SPECIES_KAKUNA", "Kakuna"),
|
||||
("SPECIES_BEEDRILL", "Beedrill"),
|
||||
("SPECIES_PIDGEY", "Pidgey"),
|
||||
("SPECIES_PIDGEOTTO", "Pidgeotto"),
|
||||
("SPECIES_PIDGEOT", "Pidgeot"),
|
||||
("SPECIES_RATTATA", "Rattata"),
|
||||
("SPECIES_RATICATE", "Raticate"),
|
||||
("SPECIES_SPEAROW", "Spearow"),
|
||||
("SPECIES_FEAROW", "Fearow"),
|
||||
("SPECIES_EKANS", "Ekans"),
|
||||
("SPECIES_ARBOK", "Arbok"),
|
||||
("SPECIES_PIKACHU", "Pikachu"),
|
||||
("SPECIES_RAICHU", "Raichu"),
|
||||
("SPECIES_SANDSHREW", "Sandshrew"),
|
||||
("SPECIES_SANDSLASH", "Sandslash"),
|
||||
("SPECIES_NIDORAN_F", "Nidoran Female"),
|
||||
("SPECIES_NIDORINA", "Nidorina"),
|
||||
("SPECIES_NIDOQUEEN", "Nidoqueen"),
|
||||
("SPECIES_NIDORAN_M", "Nidoran Male"),
|
||||
("SPECIES_NIDORINO", "Nidorino"),
|
||||
("SPECIES_NIDOKING", "Nidoking"),
|
||||
("SPECIES_CLEFAIRY", "Clefairy"),
|
||||
("SPECIES_CLEFABLE", "Clefable"),
|
||||
("SPECIES_VULPIX", "Vulpix"),
|
||||
("SPECIES_NINETALES", "Ninetales"),
|
||||
("SPECIES_JIGGLYPUFF", "Jigglypuff"),
|
||||
("SPECIES_WIGGLYTUFF", "Wigglytuff"),
|
||||
("SPECIES_ZUBAT", "Zubat"),
|
||||
("SPECIES_GOLBAT", "Golbat"),
|
||||
("SPECIES_ODDISH", "Oddish"),
|
||||
("SPECIES_GLOOM", "Gloom"),
|
||||
("SPECIES_VILEPLUME", "Vileplume"),
|
||||
("SPECIES_PARAS", "Paras"),
|
||||
("SPECIES_PARASECT", "Parasect"),
|
||||
("SPECIES_VENONAT", "Venonat"),
|
||||
("SPECIES_VENOMOTH", "Venomoth"),
|
||||
("SPECIES_DIGLETT", "Diglett"),
|
||||
("SPECIES_DUGTRIO", "Dugtrio"),
|
||||
("SPECIES_MEOWTH", "Meowth"),
|
||||
("SPECIES_PERSIAN", "Persian"),
|
||||
("SPECIES_PSYDUCK", "Psyduck"),
|
||||
("SPECIES_GOLDUCK", "Golduck"),
|
||||
("SPECIES_MANKEY", "Mankey"),
|
||||
("SPECIES_PRIMEAPE", "Primeape"),
|
||||
("SPECIES_GROWLITHE", "Growlithe"),
|
||||
("SPECIES_ARCANINE", "Arcanine"),
|
||||
("SPECIES_POLIWAG", "Poliwag"),
|
||||
("SPECIES_POLIWHIRL", "Poliwhirl"),
|
||||
("SPECIES_POLIWRATH", "Poliwrath"),
|
||||
("SPECIES_ABRA", "Abra"),
|
||||
("SPECIES_KADABRA", "Kadabra"),
|
||||
("SPECIES_ALAKAZAM", "Alakazam"),
|
||||
("SPECIES_MACHOP", "Machop"),
|
||||
("SPECIES_MACHOKE", "Machoke"),
|
||||
("SPECIES_MACHAMP", "Machamp"),
|
||||
("SPECIES_BELLSPROUT", "Bellsprout"),
|
||||
("SPECIES_WEEPINBELL", "Weepinbell"),
|
||||
("SPECIES_VICTREEBEL", "Victreebel"),
|
||||
("SPECIES_TENTACOOL", "Tentacool"),
|
||||
("SPECIES_TENTACRUEL", "Tentacruel"),
|
||||
("SPECIES_GEODUDE", "Geodude"),
|
||||
("SPECIES_GRAVELER", "Graveler"),
|
||||
("SPECIES_GOLEM", "Golem"),
|
||||
("SPECIES_PONYTA", "Ponyta"),
|
||||
("SPECIES_RAPIDASH", "Rapidash"),
|
||||
("SPECIES_SLOWPOKE", "Slowpoke"),
|
||||
("SPECIES_SLOWBRO", "Slowbro"),
|
||||
("SPECIES_MAGNEMITE", "Magnemite"),
|
||||
("SPECIES_MAGNETON", "Magneton"),
|
||||
("SPECIES_FARFETCHD", "Farfetch'd"),
|
||||
("SPECIES_DODUO", "Doduo"),
|
||||
("SPECIES_DODRIO", "Dodrio"),
|
||||
("SPECIES_SEEL", "Seel"),
|
||||
("SPECIES_DEWGONG", "Dewgong"),
|
||||
("SPECIES_GRIMER", "Grimer"),
|
||||
("SPECIES_MUK", "Muk"),
|
||||
("SPECIES_SHELLDER", "Shellder"),
|
||||
("SPECIES_CLOYSTER", "Cloyster"),
|
||||
("SPECIES_GASTLY", "Gastly"),
|
||||
("SPECIES_HAUNTER", "Haunter"),
|
||||
("SPECIES_GENGAR", "Gengar"),
|
||||
("SPECIES_ONIX", "Onix"),
|
||||
("SPECIES_DROWZEE", "Drowzee"),
|
||||
("SPECIES_HYPNO", "Hypno"),
|
||||
("SPECIES_KRABBY", "Krabby"),
|
||||
("SPECIES_KINGLER", "Kingler"),
|
||||
("SPECIES_VOLTORB", "Voltorb"),
|
||||
("SPECIES_ELECTRODE", "Electrode"),
|
||||
("SPECIES_EXEGGCUTE", "Exeggcute"),
|
||||
("SPECIES_EXEGGUTOR", "Exeggutor"),
|
||||
("SPECIES_CUBONE", "Cubone"),
|
||||
("SPECIES_MAROWAK", "Marowak"),
|
||||
("SPECIES_HITMONLEE", "Hitmonlee"),
|
||||
("SPECIES_HITMONCHAN", "Hitmonchan"),
|
||||
("SPECIES_LICKITUNG", "Lickitung"),
|
||||
("SPECIES_KOFFING", "Koffing"),
|
||||
("SPECIES_WEEZING", "Weezing"),
|
||||
("SPECIES_RHYHORN", "Rhyhorn"),
|
||||
("SPECIES_RHYDON", "Rhydon"),
|
||||
("SPECIES_CHANSEY", "Chansey"),
|
||||
("SPECIES_TANGELA", "Tangela"),
|
||||
("SPECIES_KANGASKHAN", "Kangaskhan"),
|
||||
("SPECIES_HORSEA", "Horsea"),
|
||||
("SPECIES_SEADRA", "Seadra"),
|
||||
("SPECIES_GOLDEEN", "Goldeen"),
|
||||
("SPECIES_SEAKING", "Seaking"),
|
||||
("SPECIES_STARYU", "Staryu"),
|
||||
("SPECIES_STARMIE", "Starmie"),
|
||||
("SPECIES_MR_MIME", "Mr. Mime"),
|
||||
("SPECIES_SCYTHER", "Scyther"),
|
||||
("SPECIES_JYNX", "Jynx"),
|
||||
("SPECIES_ELECTABUZZ", "Electabuzz"),
|
||||
("SPECIES_MAGMAR", "Magmar"),
|
||||
("SPECIES_PINSIR", "Pinsir"),
|
||||
("SPECIES_TAUROS", "Tauros"),
|
||||
("SPECIES_MAGIKARP", "Magikarp"),
|
||||
("SPECIES_GYARADOS", "Gyarados"),
|
||||
("SPECIES_LAPRAS", "Lapras"),
|
||||
("SPECIES_DITTO", "Ditto"),
|
||||
("SPECIES_EEVEE", "Eevee"),
|
||||
("SPECIES_VAPOREON", "Vaporeon"),
|
||||
("SPECIES_JOLTEON", "Jolteon"),
|
||||
("SPECIES_FLAREON", "Flareon"),
|
||||
("SPECIES_PORYGON", "Porygon"),
|
||||
("SPECIES_OMANYTE", "Omanyte"),
|
||||
("SPECIES_OMASTAR", "Omastar"),
|
||||
("SPECIES_KABUTO", "Kabuto"),
|
||||
("SPECIES_KABUTOPS", "Kabutops"),
|
||||
("SPECIES_AERODACTYL", "Aerodactyl"),
|
||||
("SPECIES_SNORLAX", "Snorlax"),
|
||||
("SPECIES_ARTICUNO", "Articuno"),
|
||||
("SPECIES_ZAPDOS", "Zapdos"),
|
||||
("SPECIES_MOLTRES", "Moltres"),
|
||||
("SPECIES_DRATINI", "Dratini"),
|
||||
("SPECIES_DRAGONAIR", "Dragonair"),
|
||||
("SPECIES_DRAGONITE", "Dragonite"),
|
||||
("SPECIES_MEWTWO", "Mewtwo"),
|
||||
("SPECIES_MEW", "Mew"),
|
||||
("SPECIES_CHIKORITA", "Chikorita"),
|
||||
("SPECIES_BAYLEEF", "Bayleaf"),
|
||||
("SPECIES_MEGANIUM", "Meganium"),
|
||||
("SPECIES_CYNDAQUIL", "Cindaquil"),
|
||||
("SPECIES_QUILAVA", "Quilava"),
|
||||
("SPECIES_TYPHLOSION", "Typhlosion"),
|
||||
("SPECIES_TOTODILE", "Totodile"),
|
||||
("SPECIES_CROCONAW", "Croconaw"),
|
||||
("SPECIES_FERALIGATR", "Feraligatr"),
|
||||
("SPECIES_SENTRET", "Sentret"),
|
||||
("SPECIES_FURRET", "Furret"),
|
||||
("SPECIES_HOOTHOOT", "Hoothoot"),
|
||||
("SPECIES_NOCTOWL", "Noctowl"),
|
||||
("SPECIES_LEDYBA", "Ledyba"),
|
||||
("SPECIES_LEDIAN", "Ledian"),
|
||||
("SPECIES_SPINARAK", "Spinarak"),
|
||||
("SPECIES_ARIADOS", "Ariados"),
|
||||
("SPECIES_CROBAT", "Crobat"),
|
||||
("SPECIES_CHINCHOU", "Chinchou"),
|
||||
("SPECIES_LANTURN", "Lanturn"),
|
||||
("SPECIES_PICHU", "Pichu"),
|
||||
("SPECIES_CLEFFA", "Cleffa"),
|
||||
("SPECIES_IGGLYBUFF", "Igglybuff"),
|
||||
("SPECIES_TOGEPI", "Togepi"),
|
||||
("SPECIES_TOGETIC", "Togetic"),
|
||||
("SPECIES_NATU", "Natu"),
|
||||
("SPECIES_XATU", "Xatu"),
|
||||
("SPECIES_MAREEP", "Mareep"),
|
||||
("SPECIES_FLAAFFY", "Flaafy"),
|
||||
("SPECIES_AMPHAROS", "Ampharos"),
|
||||
("SPECIES_BELLOSSOM", "Bellossom"),
|
||||
("SPECIES_MARILL", "Marill"),
|
||||
("SPECIES_AZUMARILL", "Azumarill"),
|
||||
("SPECIES_SUDOWOODO", "Sudowoodo"),
|
||||
("SPECIES_POLITOED", "Politoed"),
|
||||
("SPECIES_HOPPIP", "Hoppip"),
|
||||
("SPECIES_SKIPLOOM", "Skiploom"),
|
||||
("SPECIES_JUMPLUFF", "Jumpluff"),
|
||||
("SPECIES_AIPOM", "Aipom"),
|
||||
("SPECIES_SUNKERN", "Sunkern"),
|
||||
("SPECIES_SUNFLORA", "Sunflora"),
|
||||
("SPECIES_YANMA", "Yanma"),
|
||||
("SPECIES_WOOPER", "Wooper"),
|
||||
("SPECIES_QUAGSIRE", "Quagsire"),
|
||||
("SPECIES_ESPEON", "Espeon"),
|
||||
("SPECIES_UMBREON", "Umbreon"),
|
||||
("SPECIES_MURKROW", "Murkrow"),
|
||||
("SPECIES_SLOWKING", "Slowking"),
|
||||
("SPECIES_MISDREAVUS", "Misdreavus"),
|
||||
("SPECIES_UNOWN", "Unown"),
|
||||
("SPECIES_WOBBUFFET", "Wobbuffet"),
|
||||
("SPECIES_GIRAFARIG", "Girafarig"),
|
||||
("SPECIES_PINECO", "Pineco"),
|
||||
("SPECIES_FORRETRESS", "Forretress"),
|
||||
("SPECIES_DUNSPARCE", "Dunsparce"),
|
||||
("SPECIES_GLIGAR", "Gligar"),
|
||||
("SPECIES_STEELIX", "Steelix"),
|
||||
("SPECIES_SNUBBULL", "Snubbull"),
|
||||
("SPECIES_GRANBULL", "Granbull"),
|
||||
("SPECIES_QWILFISH", "Qwilfish"),
|
||||
("SPECIES_SCIZOR", "Scizor"),
|
||||
("SPECIES_SHUCKLE", "Shuckle"),
|
||||
("SPECIES_HERACROSS", "Heracross"),
|
||||
("SPECIES_SNEASEL", "Sneasel"),
|
||||
("SPECIES_TEDDIURSA", "Teddiursa"),
|
||||
("SPECIES_URSARING", "Ursaring"),
|
||||
("SPECIES_SLUGMA", "Slugma"),
|
||||
("SPECIES_MAGCARGO", "Magcargo"),
|
||||
("SPECIES_SWINUB", "Swinub"),
|
||||
("SPECIES_PILOSWINE", "Piloswine"),
|
||||
("SPECIES_CORSOLA", "Corsola"),
|
||||
("SPECIES_REMORAID", "Remoraid"),
|
||||
("SPECIES_OCTILLERY", "Octillery"),
|
||||
("SPECIES_DELIBIRD", "Delibird"),
|
||||
("SPECIES_MANTINE", "Mantine"),
|
||||
("SPECIES_SKARMORY", "Skarmory"),
|
||||
("SPECIES_HOUNDOUR", "Houndour"),
|
||||
("SPECIES_HOUNDOOM", "Houndoom"),
|
||||
("SPECIES_KINGDRA", "Kingdra"),
|
||||
("SPECIES_PHANPY", "Phanpy"),
|
||||
("SPECIES_DONPHAN", "Donphan"),
|
||||
("SPECIES_PORYGON2", "Porygon2"),
|
||||
("SPECIES_STANTLER", "Stantler"),
|
||||
("SPECIES_SMEARGLE", "Smeargle"),
|
||||
("SPECIES_TYROGUE", "Tyrogue"),
|
||||
("SPECIES_HITMONTOP", "Hitmontop"),
|
||||
("SPECIES_SMOOCHUM", "Smoochum"),
|
||||
("SPECIES_ELEKID", "Elekid"),
|
||||
("SPECIES_MAGBY", "Magby"),
|
||||
("SPECIES_MILTANK", "Miltank"),
|
||||
("SPECIES_BLISSEY", "Blissey"),
|
||||
("SPECIES_RAIKOU", "Raikou"),
|
||||
("SPECIES_ENTEI", "Entei"),
|
||||
("SPECIES_SUICUNE", "Suicune"),
|
||||
("SPECIES_LARVITAR", "Larvitar"),
|
||||
("SPECIES_PUPITAR", "Pupitar"),
|
||||
("SPECIES_TYRANITAR", "Tyranitar"),
|
||||
("SPECIES_LUGIA", "Lugia"),
|
||||
("SPECIES_HO_OH", "Ho-oh"),
|
||||
("SPECIES_CELEBI", "Celebi"),
|
||||
("SPECIES_TREECKO", "Treecko"),
|
||||
("SPECIES_GROVYLE", "Grovyle"),
|
||||
("SPECIES_SCEPTILE", "Sceptile"),
|
||||
("SPECIES_TORCHIC", "Torchic"),
|
||||
("SPECIES_COMBUSKEN", "Combusken"),
|
||||
("SPECIES_BLAZIKEN", "Blaziken"),
|
||||
("SPECIES_MUDKIP", "Mudkip"),
|
||||
("SPECIES_MARSHTOMP", "Marshtomp"),
|
||||
("SPECIES_SWAMPERT", "Swampert"),
|
||||
("SPECIES_POOCHYENA", "Poochyena"),
|
||||
("SPECIES_MIGHTYENA", "Mightyena"),
|
||||
("SPECIES_ZIGZAGOON", "Zigzagoon"),
|
||||
("SPECIES_LINOONE", "Linoon"),
|
||||
("SPECIES_WURMPLE", "Wurmple"),
|
||||
("SPECIES_SILCOON", "Silcoon"),
|
||||
("SPECIES_BEAUTIFLY", "Beautifly"),
|
||||
("SPECIES_CASCOON", "Cascoon"),
|
||||
("SPECIES_DUSTOX", "Dustox"),
|
||||
("SPECIES_LOTAD", "Lotad"),
|
||||
("SPECIES_LOMBRE", "Lombre"),
|
||||
("SPECIES_LUDICOLO", "Ludicolo"),
|
||||
("SPECIES_SEEDOT", "Seedot"),
|
||||
("SPECIES_NUZLEAF", "Nuzleaf"),
|
||||
("SPECIES_SHIFTRY", "Shiftry"),
|
||||
("SPECIES_NINCADA", "Nincada"),
|
||||
("SPECIES_NINJASK", "Ninjask"),
|
||||
("SPECIES_SHEDINJA", "Shedinja"),
|
||||
("SPECIES_TAILLOW", "Taillow"),
|
||||
("SPECIES_SWELLOW", "Swellow"),
|
||||
("SPECIES_SHROOMISH", "Shroomish"),
|
||||
("SPECIES_BRELOOM", "Breloom"),
|
||||
("SPECIES_SPINDA", "Spinda"),
|
||||
("SPECIES_WINGULL", "Wingull"),
|
||||
("SPECIES_PELIPPER", "Pelipper"),
|
||||
("SPECIES_SURSKIT", "Surskit"),
|
||||
("SPECIES_MASQUERAIN", "Masquerain"),
|
||||
("SPECIES_WAILMER", "Wailmer"),
|
||||
("SPECIES_WAILORD", "Wailord"),
|
||||
("SPECIES_SKITTY", "Skitty"),
|
||||
("SPECIES_DELCATTY", "Delcatty"),
|
||||
("SPECIES_KECLEON", "Kecleon"),
|
||||
("SPECIES_BALTOY", "Baltoy"),
|
||||
("SPECIES_CLAYDOL", "Claydol"),
|
||||
("SPECIES_NOSEPASS", "Nosepass"),
|
||||
("SPECIES_TORKOAL", "Torkoal"),
|
||||
("SPECIES_SABLEYE", "Sableye"),
|
||||
("SPECIES_BARBOACH", "Barboach"),
|
||||
("SPECIES_WHISCASH", "Whiscash"),
|
||||
("SPECIES_LUVDISC", "Luvdisc"),
|
||||
("SPECIES_CORPHISH", "Corphish"),
|
||||
("SPECIES_CRAWDAUNT", "Crawdaunt"),
|
||||
("SPECIES_FEEBAS", "Feebas"),
|
||||
("SPECIES_MILOTIC", "Milotic"),
|
||||
("SPECIES_CARVANHA", "Carvanha"),
|
||||
("SPECIES_SHARPEDO", "Sharpedo"),
|
||||
("SPECIES_TRAPINCH", "Trapinch"),
|
||||
("SPECIES_VIBRAVA", "Vibrava"),
|
||||
("SPECIES_FLYGON", "Flygon"),
|
||||
("SPECIES_MAKUHITA", "Makuhita"),
|
||||
("SPECIES_HARIYAMA", "Hariyama"),
|
||||
("SPECIES_ELECTRIKE", "Electrike"),
|
||||
("SPECIES_MANECTRIC", "Manectric"),
|
||||
("SPECIES_NUMEL", "Numel"),
|
||||
("SPECIES_CAMERUPT", "Camerupt"),
|
||||
("SPECIES_SPHEAL", "Spheal"),
|
||||
("SPECIES_SEALEO", "Sealeo"),
|
||||
("SPECIES_WALREIN", "Walrein"),
|
||||
("SPECIES_CACNEA", "Cacnea"),
|
||||
("SPECIES_CACTURNE", "Cacturne"),
|
||||
("SPECIES_SNORUNT", "Snorunt"),
|
||||
("SPECIES_GLALIE", "Glalie"),
|
||||
("SPECIES_LUNATONE", "Lunatone"),
|
||||
("SPECIES_SOLROCK", "Solrock"),
|
||||
("SPECIES_AZURILL", "Azurill"),
|
||||
("SPECIES_SPOINK", "Spoink"),
|
||||
("SPECIES_GRUMPIG", "Grumpig"),
|
||||
("SPECIES_PLUSLE", "Plusle"),
|
||||
("SPECIES_MINUN", "Minun"),
|
||||
("SPECIES_MAWILE", "Mawile"),
|
||||
("SPECIES_MEDITITE", "Meditite"),
|
||||
("SPECIES_MEDICHAM", "Medicham"),
|
||||
("SPECIES_SWABLU", "Swablu"),
|
||||
("SPECIES_ALTARIA", "Altaria"),
|
||||
("SPECIES_WYNAUT", "Wynaut"),
|
||||
("SPECIES_DUSKULL", "Duskull"),
|
||||
("SPECIES_DUSCLOPS", "Dusclops"),
|
||||
("SPECIES_ROSELIA", "Roselia"),
|
||||
("SPECIES_SLAKOTH", "Slakoth"),
|
||||
("SPECIES_VIGOROTH", "Vigoroth"),
|
||||
("SPECIES_SLAKING", "Slaking"),
|
||||
("SPECIES_GULPIN", "Gulpin"),
|
||||
("SPECIES_SWALOT", "Swalot"),
|
||||
("SPECIES_TROPIUS", "Tropius"),
|
||||
("SPECIES_WHISMUR", "Whismur"),
|
||||
("SPECIES_LOUDRED", "Loudred"),
|
||||
("SPECIES_EXPLOUD", "Exploud"),
|
||||
("SPECIES_CLAMPERL", "Clamperl"),
|
||||
("SPECIES_HUNTAIL", "Huntail"),
|
||||
("SPECIES_GOREBYSS", "Gorebyss"),
|
||||
("SPECIES_ABSOL", "Absol"),
|
||||
("SPECIES_SHUPPET", "Shuppet"),
|
||||
("SPECIES_BANETTE", "Banette"),
|
||||
("SPECIES_SEVIPER", "Seviper"),
|
||||
("SPECIES_ZANGOOSE", "Zangoose"),
|
||||
("SPECIES_RELICANTH", "Relicanth"),
|
||||
("SPECIES_ARON", "Aron"),
|
||||
("SPECIES_LAIRON", "Lairon"),
|
||||
("SPECIES_AGGRON", "Aggron"),
|
||||
("SPECIES_CASTFORM", "Castform"),
|
||||
("SPECIES_VOLBEAT", "Volbeat"),
|
||||
("SPECIES_ILLUMISE", "Illumise"),
|
||||
("SPECIES_LILEEP", "Lileep"),
|
||||
("SPECIES_CRADILY", "Cradily"),
|
||||
("SPECIES_ANORITH", "Anorith"),
|
||||
("SPECIES_ARMALDO", "Armaldo"),
|
||||
("SPECIES_RALTS", "Ralts"),
|
||||
("SPECIES_KIRLIA", "Kirlia"),
|
||||
("SPECIES_GARDEVOIR", "Gardevoir"),
|
||||
("SPECIES_BAGON", "Bagon"),
|
||||
("SPECIES_SHELGON", "Shelgon"),
|
||||
("SPECIES_SALAMENCE", "Salamence"),
|
||||
("SPECIES_BELDUM", "Beldum"),
|
||||
("SPECIES_METANG", "Metang"),
|
||||
("SPECIES_METAGROSS", "Metagross"),
|
||||
("SPECIES_REGIROCK", "Regirock"),
|
||||
("SPECIES_REGICE", "Regice"),
|
||||
("SPECIES_REGISTEEL", "Registeel"),
|
||||
("SPECIES_KYOGRE", "Kyogre"),
|
||||
("SPECIES_GROUDON", "Groudon"),
|
||||
("SPECIES_RAYQUAZA", "Rayquaza"),
|
||||
("SPECIES_LATIAS", "Latias"),
|
||||
("SPECIES_LATIOS", "Latios"),
|
||||
("SPECIES_JIRACHI", "Jirachi"),
|
||||
("SPECIES_DEOXYS", "Deoxys"),
|
||||
("SPECIES_CHIMECHO", "Chimecho")
|
||||
]
|
||||
|
||||
species_list: List[SpeciesData] = []
|
||||
max_species_id = 0
|
||||
for species_name, species_label in all_species:
|
||||
species_id = data.constants[species_name]
|
||||
max_species_id = max(species_id, max_species_id)
|
||||
species_data = extracted_data["species"][species_id]
|
||||
|
||||
learnset = [LearnsetMove(item["level"], item["move_id"]) for item in species_data["learnset"]["moves"]]
|
||||
|
||||
species_list.append(SpeciesData(
|
||||
species_name,
|
||||
species_label,
|
||||
species_id,
|
||||
BaseStats(
|
||||
species_data["base_stats"][0],
|
||||
species_data["base_stats"][1],
|
||||
species_data["base_stats"][2],
|
||||
species_data["base_stats"][3],
|
||||
species_data["base_stats"][4],
|
||||
species_data["base_stats"][5]
|
||||
),
|
||||
(species_data["types"][0], species_data["types"][1]),
|
||||
(species_data["abilities"][0], species_data["abilities"][1]),
|
||||
[EvolutionData(
|
||||
_str_to_evolution_method(evolution_json["method"]),
|
||||
evolution_json["param"],
|
||||
evolution_json["species"],
|
||||
) for evolution_json in species_data["evolutions"]],
|
||||
None,
|
||||
species_data["catch_rate"],
|
||||
learnset,
|
||||
int(species_data["tmhm_learnset"], 16),
|
||||
species_data["learnset"]["rom_address"],
|
||||
species_data["rom_address"]
|
||||
))
|
||||
|
||||
data.species = [None for i in range(max_species_id + 1)]
|
||||
|
||||
for species_data in species_list:
|
||||
data.species[species_data.species_id] = species_data
|
||||
|
||||
for species in data.species:
|
||||
if species is not None:
|
||||
for evolution in species.evolutions:
|
||||
data.species[evolution.species_id].pre_evolution = species.species_id
|
||||
|
||||
# Create static encounter data
|
||||
for static_encounter_json in extracted_data["static_encounters"]:
|
||||
data.static_encounters.append(StaticEncounterData(
|
||||
static_encounter_json["species"],
|
||||
static_encounter_json["rom_address"]
|
||||
))
|
||||
|
||||
# TM moves
|
||||
data.tmhm_moves = extracted_data["tmhm_moves"]
|
||||
|
||||
# Create ability data
|
||||
data.abilities = [AbilityData(data.constants[ability_data[0]], ability_data[1]) for ability_data in [
|
||||
("ABILITY_STENCH", "Stench"),
|
||||
("ABILITY_DRIZZLE", "Drizzle"),
|
||||
("ABILITY_SPEED_BOOST", "Speed Boost"),
|
||||
("ABILITY_BATTLE_ARMOR", "Battle Armor"),
|
||||
("ABILITY_STURDY", "Sturdy"),
|
||||
("ABILITY_DAMP", "Damp"),
|
||||
("ABILITY_LIMBER", "Limber"),
|
||||
("ABILITY_SAND_VEIL", "Sand Veil"),
|
||||
("ABILITY_STATIC", "Static"),
|
||||
("ABILITY_VOLT_ABSORB", "Volt Absorb"),
|
||||
("ABILITY_WATER_ABSORB", "Water Absorb"),
|
||||
("ABILITY_OBLIVIOUS", "Oblivious"),
|
||||
("ABILITY_CLOUD_NINE", "Cloud Nine"),
|
||||
("ABILITY_COMPOUND_EYES", "Compound Eyes"),
|
||||
("ABILITY_INSOMNIA", "Insomnia"),
|
||||
("ABILITY_COLOR_CHANGE", "Color Change"),
|
||||
("ABILITY_IMMUNITY", "Immunity"),
|
||||
("ABILITY_FLASH_FIRE", "Flash Fire"),
|
||||
("ABILITY_SHIELD_DUST", "Shield Dust"),
|
||||
("ABILITY_OWN_TEMPO", "Own Tempo"),
|
||||
("ABILITY_SUCTION_CUPS", "Suction Cups"),
|
||||
("ABILITY_INTIMIDATE", "Intimidate"),
|
||||
("ABILITY_SHADOW_TAG", "Shadow Tag"),
|
||||
("ABILITY_ROUGH_SKIN", "Rough Skin"),
|
||||
("ABILITY_WONDER_GUARD", "Wonder Guard"),
|
||||
("ABILITY_LEVITATE", "Levitate"),
|
||||
("ABILITY_EFFECT_SPORE", "Effect Spore"),
|
||||
("ABILITY_SYNCHRONIZE", "Synchronize"),
|
||||
("ABILITY_CLEAR_BODY", "Clear Body"),
|
||||
("ABILITY_NATURAL_CURE", "Natural Cure"),
|
||||
("ABILITY_LIGHTNING_ROD", "Lightning Rod"),
|
||||
("ABILITY_SERENE_GRACE", "Serene Grace"),
|
||||
("ABILITY_SWIFT_SWIM", "Swift Swim"),
|
||||
("ABILITY_CHLOROPHYLL", "Chlorophyll"),
|
||||
("ABILITY_ILLUMINATE", "Illuminate"),
|
||||
("ABILITY_TRACE", "Trace"),
|
||||
("ABILITY_HUGE_POWER", "Huge Power"),
|
||||
("ABILITY_POISON_POINT", "Poison Point"),
|
||||
("ABILITY_INNER_FOCUS", "Inner Focus"),
|
||||
("ABILITY_MAGMA_ARMOR", "Magma Armor"),
|
||||
("ABILITY_WATER_VEIL", "Water Veil"),
|
||||
("ABILITY_MAGNET_PULL", "Magnet Pull"),
|
||||
("ABILITY_SOUNDPROOF", "Soundproof"),
|
||||
("ABILITY_RAIN_DISH", "Rain Dish"),
|
||||
("ABILITY_SAND_STREAM", "Sand Stream"),
|
||||
("ABILITY_PRESSURE", "Pressure"),
|
||||
("ABILITY_THICK_FAT", "Thick Fat"),
|
||||
("ABILITY_EARLY_BIRD", "Early Bird"),
|
||||
("ABILITY_FLAME_BODY", "Flame Body"),
|
||||
("ABILITY_RUN_AWAY", "Run Away"),
|
||||
("ABILITY_KEEN_EYE", "Keen Eye"),
|
||||
("ABILITY_HYPER_CUTTER", "Hyper Cutter"),
|
||||
("ABILITY_PICKUP", "Pickup"),
|
||||
("ABILITY_TRUANT", "Truant"),
|
||||
("ABILITY_HUSTLE", "Hustle"),
|
||||
("ABILITY_CUTE_CHARM", "Cute Charm"),
|
||||
("ABILITY_PLUS", "Plus"),
|
||||
("ABILITY_MINUS", "Minus"),
|
||||
("ABILITY_FORECAST", "Forecast"),
|
||||
("ABILITY_STICKY_HOLD", "Sticky Hold"),
|
||||
("ABILITY_SHED_SKIN", "Shed Skin"),
|
||||
("ABILITY_GUTS", "Guts"),
|
||||
("ABILITY_MARVEL_SCALE", "Marvel Scale"),
|
||||
("ABILITY_LIQUID_OOZE", "Liquid Ooze"),
|
||||
("ABILITY_OVERGROW", "Overgrow"),
|
||||
("ABILITY_BLAZE", "Blaze"),
|
||||
("ABILITY_TORRENT", "Torrent"),
|
||||
("ABILITY_SWARM", "Swarm"),
|
||||
("ABILITY_ROCK_HEAD", "Rock Head"),
|
||||
("ABILITY_DROUGHT", "Drought"),
|
||||
("ABILITY_ARENA_TRAP", "Arena Trap"),
|
||||
("ABILITY_VITAL_SPIRIT", "Vital Spirit"),
|
||||
("ABILITY_WHITE_SMOKE", "White Smoke"),
|
||||
("ABILITY_PURE_POWER", "Pure Power"),
|
||||
("ABILITY_SHELL_ARMOR", "Shell Armor"),
|
||||
("ABILITY_CACOPHONY", "Cacophony"),
|
||||
("ABILITY_AIR_LOCK", "Air Lock")
|
||||
]]
|
||||
|
||||
# Create map data
|
||||
for map_name, map_json in extracted_data["maps"].items():
|
||||
land_encounters = None
|
||||
water_encounters = None
|
||||
fishing_encounters = None
|
||||
|
||||
if map_json["land_encounters"] is not None:
|
||||
land_encounters = EncounterTableData(
|
||||
map_json["land_encounters"]["encounter_slots"],
|
||||
map_json["land_encounters"]["rom_address"]
|
||||
)
|
||||
if map_json["water_encounters"] is not None:
|
||||
water_encounters = EncounterTableData(
|
||||
map_json["water_encounters"]["encounter_slots"],
|
||||
map_json["water_encounters"]["rom_address"]
|
||||
)
|
||||
if map_json["fishing_encounters"] is not None:
|
||||
fishing_encounters = EncounterTableData(
|
||||
map_json["fishing_encounters"]["encounter_slots"],
|
||||
map_json["fishing_encounters"]["rom_address"]
|
||||
)
|
||||
|
||||
data.maps.append(MapData(
|
||||
map_name,
|
||||
land_encounters,
|
||||
water_encounters,
|
||||
fishing_encounters
|
||||
))
|
||||
|
||||
data.maps.sort(key=lambda map: map.name)
|
||||
|
||||
# Create warp map
|
||||
for warp, destination in extracted_data["warps"].items():
|
||||
data.warp_map[warp] = None if destination == "" else destination
|
||||
|
||||
if encoded_warp not in data.warp_map:
|
||||
data.warp_map[encoded_warp] = None
|
||||
|
||||
# Create trainer data
|
||||
for i, trainer_json in enumerate(extracted_data["trainers"]):
|
||||
party_json = trainer_json["party"]
|
||||
pokemon_data_type = _str_to_pokemon_data_type(trainer_json["pokemon_data_type"])
|
||||
data.trainers.append(TrainerData(
|
||||
i,
|
||||
TrainerPartyData(
|
||||
[TrainerPokemonData(
|
||||
p["species"],
|
||||
p["level"],
|
||||
(p["moves"][0], p["moves"][1], p["moves"][2], p["moves"][3])
|
||||
) for p in party_json],
|
||||
pokemon_data_type,
|
||||
trainer_json["party_rom_address"]
|
||||
),
|
||||
trainer_json["rom_address"],
|
||||
trainer_json["battle_script_rom_address"]
|
||||
))
|
||||
|
||||
|
||||
_init()
|
|
@ -0,0 +1,99 @@
|
|||
## `regions/`
|
||||
|
||||
These define regions, connections, and where locations are. If you know what you're doing, it should be pretty clear how
|
||||
this works by taking a quick look through the files. The rest of this section is pretty verbose to cover everything. Not
|
||||
to say you shouldn't read it, but the tl;dr is:
|
||||
|
||||
- Every map, even trivial ones, gets a region definition, and they cannot be coalesced (because of warp rando)
|
||||
- Stick to the naming convention for regions and events (look at Route 103 and Petalburg City for guidance)
|
||||
- Locations and warps can only be claimed by one region
|
||||
- Events are declared here
|
||||
|
||||
A `Map`, which you will see referenced in `parent_map` attribute in the region JSON, is an id from the source code.
|
||||
`Map`s are sets of tiles, encounters, warps, events, and so on. Route 103, Littleroot Town, the Oldale Town Mart, the
|
||||
second floor of Devon Corp, and each level of Victory Road are all examples of `Map`s. You transition between `Map`s by
|
||||
stepping on a warp (warp pads, doorways, etc...) or walking over a border between `Map`s in the overworld. Some warps
|
||||
don't go to a different `Map`.
|
||||
|
||||
Regions usually describe physical areas which are subsets of a `Map`. Every `Map` must have one or more defined regions.
|
||||
A region should not contain area from more than one `Map`. We'll need to draw those lines now even when there is no
|
||||
logical boundary (like between two the first and second floors of your rival's house), for warp rando.
|
||||
|
||||
Most `Map`s have been split into multiple regions. In the example below, `MAP_ROUTE103` was split into
|
||||
`REGION_ROUTE_103/WEST`, `REGION_ROUTE_103/WATER`, and `REGION_ROUTE_103/EAST` (this document may be out of date; the
|
||||
example is demonstrative). Keeping the name consistent with the `Map` name and adding a label suffix for the subarea
|
||||
makes it clearer where we are in the world and where within a `Map` we're describing.
|
||||
|
||||
Every region (except `Menu`) is configured here. All files in this directory are combined with each other at runtime,
|
||||
and are only split and ordered for organization. Regions defined in `data/regions/unused` are entirely unused because
|
||||
they're not yet reachable in the randomizer. They're there for future reference in case we want to pull those maps in
|
||||
later. Any locations or warps in here should be ignored. Data for a single region looks like this:
|
||||
|
||||
```json
|
||||
"REGION_ROUTE103/EAST": {
|
||||
"parent_map": "MAP_ROUTE103",
|
||||
"locations": [
|
||||
"ITEM_ROUTE_103_GUARD_SPEC",
|
||||
"ITEM_ROUTE_103_PP_UP"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_ROUTE103/WATER",
|
||||
"REGION_ROUTE110/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_ROUTE103:0/MAP_ALTERING_CAVE:0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `[key]`: The name of the object, in this case `REGION_ROUTE103/EAST`, should be the value of `parent_map` where the
|
||||
`MAP` prefix is replaced with `REGION`. Then there should be a following `/` and a label describing this specific region
|
||||
within the `Map`. This is not enforced or required by the code, but it makes things much more clear.
|
||||
- `parent_map`: The name of the `Map` this region exists under. It can relate this region to information like encounter
|
||||
tables.
|
||||
- `locations`: Locations contained within this region. This can be anything from an item on the ground to a badge to a
|
||||
gift from an NPC. Locations themselves are defined in `data/extracted_data.json`, and the names used here should come
|
||||
directly from it.
|
||||
- `events`: Events that can be completed in this region. Defeating a gym leader or Aqua/Magma team leader, for example,
|
||||
can trigger story progression and unblock roads and buildings. Events are defined here and nowhere else, and access
|
||||
rules are set in `rules.py`.
|
||||
- `exits`: Names of regions that can be directly accessed from this one. Most often regions within the same `Map`,
|
||||
neighboring maps in the overworld, or transitions from using HM08 Dive. Most connections between maps/regions come from
|
||||
warps. Any region in this list should be defined somewhere in `data/regions`.
|
||||
- `warps`: Warp events contained within this region. Warps are defined in `data/extracted_data.json`, and must exist
|
||||
there to be referenced here. More on warps in [../README.md](../README.md).
|
||||
|
||||
Think of this data as defining which regions are "claiming" a given location, event, or warp. No more than one region
|
||||
may claim ownership of a location. Even if some "thing" may happen in two different regions and set the same flag, they
|
||||
should be defined as two different events and anything conditional on said "thing" happening can check whether either of
|
||||
the two events is accessible. (e.g. Interacting with the Poke Ball in your rival's room and going back downstairs will
|
||||
both trigger a conversation with them which enables you to rescue Professor Birch. It's the same "thing" on two
|
||||
different `Map`s.)
|
||||
|
||||
Conceptually, you shouldn't have to "add" any new regions. You should only have to "split" existing regions. When you
|
||||
split a region, make sure to correctly reassign `locations`, `events`, `exits`, and `warps` according to which new
|
||||
region they now exist in. Make sure to define new `exits` to link the new regions to each other if applicable. And
|
||||
especially remember to rename incoming `exits` defined in other regions which are still pointing to the pre-split
|
||||
region. `sanity_check.py` should catch you if there are other regions that point to a region that no longer exists, but
|
||||
if one of your newly-split regions still has the same name as the original, it won't be detected and you may find that
|
||||
things aren't connected correctly.
|
||||
|
||||
## `extracted_data.json`
|
||||
|
||||
DO NOT TOUCH
|
||||
|
||||
Contains data automatically pulled from the base rom and its source code when it is built. There should be no reason to
|
||||
manually modify it. Data from this file is piped through `data.py` to create a data object that's more useful and
|
||||
complete.
|
||||
|
||||
## `items.json`
|
||||
|
||||
A map from items as defined in the `constants` in `extracted_data.json` to useful info like a human-friendly label, the
|
||||
type of progression it enables, and tags to associate. There are many unused items and extra helper constants in
|
||||
`extracted_data.json`, so this file contains an exhaustive list of items which can actually be found in the modded game.
|
||||
|
||||
## `locations.json`
|
||||
|
||||
Similar to `items.json`, this associates locations with human-friendly labels and tags that are used for filtering. Any
|
||||
locations claimed by any region need an entry here.
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,396 @@
|
|||
{
|
||||
"REGION_BATTLE_FRONTIER_RECEPTION_GATE/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_RECEPTION_GATE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8",
|
||||
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_SLATEPORT_CITY_HARBOR/MAIN",
|
||||
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER",
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE"
|
||||
],
|
||||
"warps": []
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN",
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/CAVE_ENTRANCE": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN",
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER"
|
||||
],
|
||||
"warps": []
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL",
|
||||
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
|
||||
],
|
||||
"warps": []
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_DOME_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_RANKING_HALL/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_RANKING_HALL",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_POKEMON_CENTER_1F/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_POKEMON_CENTER_2F/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_MART/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_MART",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_SCOTTS_HOUSE/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE1/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE1",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE2/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE2",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE3/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE3",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE4/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE4",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE5/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE5",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE6/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE6",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE7/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE7",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE8/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE8",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10"
|
||||
]
|
||||
},
|
||||
"REGION_BATTLE_FRONTIER_LOUNGE9/MAIN": {
|
||||
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE9",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11"
|
||||
]
|
||||
},
|
||||
|
||||
"REGION_ARTISAN_CAVE_1F/MAIN": {
|
||||
"parent_map": "MAP_ARTISAN_CAVE_1F",
|
||||
"locations": [
|
||||
"ITEM_ARTISAN_CAVE_1F_CARBOS"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1",
|
||||
"MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13"
|
||||
]
|
||||
},
|
||||
"REGION_ARTISAN_CAVE_B1F/MAIN": {
|
||||
"parent_map": "MAP_ARTISAN_CAVE_B1F",
|
||||
"locations": [
|
||||
"ITEM_ARTISAN_CAVE_B1F_HP_UP",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10",
|
||||
"MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"REGION_TERRA_CAVE_ENTRANCE/MAIN": {
|
||||
"parent_map": "MAP_TERRA_CAVE_ENTRANCE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!",
|
||||
"MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0"
|
||||
]
|
||||
},
|
||||
"REGION_TERRA_CAVE_END/MAIN": {
|
||||
"parent_map": "MAP_TERRA_CAVE_END",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1"
|
||||
]
|
||||
},
|
||||
"REGION_UNDERWATER_MARINE_CAVE/MAIN": {
|
||||
"parent_map": "MAP_UNDERWATER_MARINE_CAVE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_MARINE_CAVE_ENTRANCE/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!"
|
||||
]
|
||||
},
|
||||
"REGION_MARINE_CAVE_ENTRANCE/MAIN": {
|
||||
"parent_map": "MAP_MARINE_CAVE_ENTRANCE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [
|
||||
"REGION_UNDERWATER_MARINE_CAVE/MAIN"
|
||||
],
|
||||
"warps": [
|
||||
"MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0"
|
||||
]
|
||||
},
|
||||
"REGION_MARINE_CAVE_END/MAIN": {
|
||||
"parent_map": "MAP_MARINE_CAVE_END",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
{
|
||||
"REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN": {
|
||||
"parent_map": "MAP_SOUTHERN_ISLAND_EXTERIOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1"
|
||||
]
|
||||
},
|
||||
"REGION_SOUTHERN_ISLAND_INTERIOR/MAIN": {
|
||||
"parent_map": "MAP_SOUTHERN_ISLAND_INTERIOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1"
|
||||
]
|
||||
},
|
||||
"REGION_FARAWAY_ISLAND_ENTRANCE/MAIN": {
|
||||
"parent_map": "MAP_FARAWAY_ISLAND_ENTRANCE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1"
|
||||
]
|
||||
},
|
||||
"REGION_FARAWAY_ISLAND_INTERIOR/MAIN": {
|
||||
"parent_map": "MAP_FARAWAY_ISLAND_INTERIOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1"
|
||||
]
|
||||
},
|
||||
"REGION_BIRTH_ISLAND_HARBOR/MAIN": {
|
||||
"parent_map": "MAP_BIRTH_ISLAND_HARBOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0"
|
||||
]
|
||||
},
|
||||
"REGION_BIRTH_ISLAND_EXTERIOR/MAIN": {
|
||||
"parent_map": "MAP_BIRTH_ISLAND_EXTERIOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_HARBOR/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_HARBOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_EXTERIOR/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_EXTERIOR",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
|
||||
"MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_ENTRANCE/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_ENTRANCE",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
|
||||
"MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_B1F/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_B1F",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
|
||||
"MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_FORK/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_FORK",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
|
||||
"MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
|
||||
"MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN01/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN01",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
|
||||
"MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN02/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN02",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
|
||||
"MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN03/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN03",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
|
||||
"MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN04/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN04",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
|
||||
"MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN05/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN05",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
|
||||
"MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN06/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN06",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
|
||||
"MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN07/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN07",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
|
||||
"MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN08/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN08",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
|
||||
"MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN09/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN09",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
|
||||
"MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN10/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN10",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
|
||||
"MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_DOWN11/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_DOWN11",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
|
||||
"MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_BOTTOM/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_BOTTOM",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_UP1/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_UP1",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
|
||||
"MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_UP2/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_UP2",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
|
||||
"MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_UP3/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_UP3",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
|
||||
"MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_UP4/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_UP4",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
|
||||
"MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0"
|
||||
]
|
||||
},
|
||||
"REGION_NAVEL_ROCK_TOP/MAIN": {
|
||||
"parent_map": "MAP_NAVEL_ROCK_TOP",
|
||||
"locations": [
|
||||
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
|
||||
],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6"
|
||||
]
|
||||
},
|
||||
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/MAIN": {
|
||||
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8",
|
||||
"locations": [],
|
||||
"events": [],
|
||||
"exits": [],
|
||||
"warps": [
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
# Pokémon Emerald
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
You can read through all the settings and generate a YAML [here](../player-settings).
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on
|
||||
the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon,
|
||||
abilities, types, etc… You can even change a percentage of single battles into double battles. Check the
|
||||
[settings page](../player-settings) for a more comprehensive list of what can be changed.
|
||||
|
||||
## What items and locations get randomized?
|
||||
|
||||
The most interesting items that can be added to the item pool are badges and HMs, which most affect what locations you
|
||||
can access. Key items like the Devon Scope or Mach Bike can also be randomized, as well as the many Potions, Revives,
|
||||
TMs, and other items that you can find on the ground or receive as gifts.
|
||||
|
||||
## What other changes are made to the game?
|
||||
|
||||
There are many quality of life improvements meant to speed up the game a little and improve the experience of playing a
|
||||
randomizer. Here are some of the more important ones:
|
||||
|
||||
- Shoal Cave switches between high tide and low tide every time you re-enter
|
||||
- Bag space is greatly expanded (you're all but guaranteed to never need to store items in the PC)
|
||||
- Trade evolutions have been changed to level or item evolutions
|
||||
- You can have both bikes simultaneously
|
||||
- You can run or bike (almost) anywhere
|
||||
- The Wally catching tutorial is skipped
|
||||
- All text is instant, and with a setting it can be automatically progressed by holding A
|
||||
- When a Repel runs out, you will be prompted to use another
|
||||
- Many more minor improvements…
|
||||
|
||||
## Where is my starting inventory?
|
||||
|
||||
Except for badges, your starting inventory will be in the PC.
|
||||
|
||||
## What does another world's item look like in Pokémon Emerald?
|
||||
|
||||
When you find an item that is not your own, you will instead receive an "ARCHIPELAGO ITEM" which will *not* be added to
|
||||
your inventory.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages`
|
||||
setting, the received item will either be silently added to your bag or you will be shown a text box with the item's
|
||||
name and the item will be added to your bag while a fanfare plays.
|
||||
|
||||
## Can I play offline?
|
||||
|
||||
Yes, the client and connector are only necessary for sending and receiving items. If you're playing a solo game, you
|
||||
don't need to play online unless you want the rest of Archipelago's functionality (like hints and auto-tracking). If
|
||||
you're playing a multiworld game, the client will sync your game with the server the next time you connect.
|
||||
|
||||
## Will battle mechanics be updated?
|
||||
|
||||
This is something we'd love to see, but it's unlikely. We don't want to force new mechanics on players who would prefer
|
||||
to play with the classic mechanics, but trying to switch between old and new mechanics based on an option would be a
|
||||
monumental task, and is probably best solved some other way.
|
||||
|
||||
## Is this randomizer compatible with other mods?
|
||||
|
||||
No, other mods cannot be applied. It would be impossible to generalize this implementation's changes in a way that is
|
||||
compatible with any other mod or romhack. Romhacks could be added as their own games, but they would have to be
|
||||
implemented separately. Check out [Archipelago's Discord server](https://discord.gg/8Z65BR2) if you want to make a
|
||||
suggestion or contribute.
|
||||
|
||||
## Can I use tools like the Universal Pokémon Randomizer?
|
||||
|
||||
No, those tools expect data to be in certain locations and in a certain format, but this randomizer has to shift it
|
||||
around. Using tools to try to modify the game would only corrupt the ROM.
|
||||
|
||||
We realize this means breaking from established habits when it comes to randomizing Pokémon games, but this randomizer
|
||||
would be many times more complex to develop if it were constrained by something like UPR.
|
||||
|
||||
The one exception might be PKHeX. You may be able to extract pokémon from your save using PKHeX, but this isn't a
|
||||
guarantee, and we make no effort to keep our saves compatible with PKHeX.
|
|
@ -0,0 +1,72 @@
|
|||
# Pokémon Emerald Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- An English Pokémon Emerald ROM. The Archipelago community cannot provide this.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
|
||||
|
||||
### Configuring BizHawk
|
||||
|
||||
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||
|
||||
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||
tabbed out of EmuHawk.
|
||||
- Open a `.gba` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||
`Controllers…`, load any `.gba` ROM first.
|
||||
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||
clear it.
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), for use with
|
||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
## Generating and Patching a Game
|
||||
|
||||
1. Create your settings file (YAML). You can make one on the
|
||||
[Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have the `.apemerald` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
4. Select "Open Patch" on the left side and select your patch file.
|
||||
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
|
||||
6. A patched `.gba` file will be created in the same place as the patch file.
|
||||
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||
BizHawk install.
|
||||
|
||||
If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the
|
||||
client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue
|
||||
below using BizHawk as your emulator.
|
||||
|
||||
## Connecting to a Server
|
||||
|
||||
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||
in case you have to close and reopen a window mid-game for some reason.
|
||||
|
||||
1. Pokemon Emerald uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
|
||||
you can re-open it from the launcher.
|
||||
2. Ensure EmuHawk is running the patched ROM.
|
||||
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
|
||||
Client window should indicate that it connected and recognized Pokemon Emerald.
|
||||
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||
top text field of the client and click Connect.
|
||||
|
||||
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
|
||||
perfectly safe to make progress offline; everything will re-sync when you reconnect.
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
Pokémon Emerald has a fully functional map tracker that supports auto-tracking.
|
||||
|
||||
1. Download [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) and
|
||||
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||
2. Put the tracker pack into packs/ in your PopTracker install.
|
||||
3. Open PopTracker, and load the Pokémon Emerald pack.
|
||||
4. For autotracking, click on the "AP" symbol at the top.
|
||||
5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
Classes and functions related to AP items for Pokemon Emerald
|
||||
"""
|
||||
from typing import Dict, FrozenSet, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
from .data import BASE_OFFSET, data
|
||||
|
||||
|
||||
class PokemonEmeraldItem(Item):
|
||||
game: str = "Pokemon Emerald"
|
||||
tags: FrozenSet[str]
|
||||
|
||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int) -> None:
|
||||
super().__init__(name, classification, code, player)
|
||||
|
||||
if code is None:
|
||||
self.tags = frozenset(["Event"])
|
||||
else:
|
||||
self.tags = data.items[reverse_offset_item_value(code)].tags
|
||||
|
||||
|
||||
def offset_item_value(item_value: int) -> int:
|
||||
"""
|
||||
Returns the AP item id (code) for a given item value
|
||||
"""
|
||||
return item_value + BASE_OFFSET
|
||||
|
||||
|
||||
def reverse_offset_item_value(item_id: int) -> int:
|
||||
"""
|
||||
Returns the item value for a given AP item id (code)
|
||||
"""
|
||||
return item_id - BASE_OFFSET
|
||||
|
||||
|
||||
def create_item_label_to_code_map() -> Dict[str, int]:
|
||||
"""
|
||||
Creates a map from item labels to their AP item id (code)
|
||||
"""
|
||||
label_to_code_map: Dict[str, int] = {}
|
||||
for item_value, attributes in data.items.items():
|
||||
label_to_code_map[attributes.label] = offset_item_value(item_value)
|
||||
|
||||
return label_to_code_map
|
||||
|
||||
|
||||
ITEM_GROUPS = {
|
||||
"Badges": {
|
||||
"Stone Badge", "Knuckle Badge",
|
||||
"Dynamo Badge", "Heat Badge",
|
||||
"Balance Badge", "Feather Badge",
|
||||
"Mind Badge", "Rain Badge"
|
||||
},
|
||||
"HMs": {
|
||||
"HM01 Cut", "HM02 Fly",
|
||||
"HM03 Surf", "HM04 Strength",
|
||||
"HM05 Flash", "HM06 Rock Smash",
|
||||
"HM07 Waterfall", "HM08 Dive"
|
||||
},
|
||||
"HM01": {"HM01 Cut"},
|
||||
"HM02": {"HM02 Fly"},
|
||||
"HM03": {"HM03 Surf"},
|
||||
"HM04": {"HM04 Strength"},
|
||||
"HM05": {"HM05 Flash"},
|
||||
"HM06": {"HM06 Rock Smash"},
|
||||
"HM07": {"HM07 Waterfall"},
|
||||
"HM08": {"HM08 Dive"}
|
||||
}
|
||||
|
||||
|
||||
def get_item_classification(item_code: int) -> ItemClassification:
|
||||
"""
|
||||
Returns the item classification for a given AP item id (code)
|
||||
"""
|
||||
return data.items[reverse_offset_item_value(item_code)].classification
|
|
@ -0,0 +1,122 @@
|
|||
"""
|
||||
Classes and functions related to AP locations for Pokemon Emerald
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, FrozenSet, Iterable
|
||||
|
||||
from BaseClasses import Location, Region
|
||||
|
||||
from .data import BASE_OFFSET, data
|
||||
from .items import offset_item_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import PokemonEmeraldWorld
|
||||
|
||||
|
||||
class PokemonEmeraldLocation(Location):
|
||||
game: str = "Pokemon Emerald"
|
||||
rom_address: Optional[int]
|
||||
default_item_code: Optional[int]
|
||||
tags: FrozenSet[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player: int,
|
||||
name: str,
|
||||
flag: Optional[int],
|
||||
parent: Optional[Region] = None,
|
||||
rom_address: Optional[int] = None,
|
||||
default_item_value: Optional[int] = None,
|
||||
tags: FrozenSet[str] = frozenset()) -> None:
|
||||
super().__init__(player, name, None if flag is None else offset_flag(flag), parent)
|
||||
self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value)
|
||||
self.rom_address = rom_address
|
||||
self.tags = tags
|
||||
|
||||
|
||||
def offset_flag(flag: int) -> int:
|
||||
"""
|
||||
Returns the AP location id (address) for a given flag
|
||||
"""
|
||||
if flag is None:
|
||||
return None
|
||||
return flag + BASE_OFFSET
|
||||
|
||||
|
||||
def reverse_offset_flag(location_id: int) -> int:
|
||||
"""
|
||||
Returns the flag id for a given AP location id (address)
|
||||
"""
|
||||
if location_id is None:
|
||||
return None
|
||||
return location_id - BASE_OFFSET
|
||||
|
||||
|
||||
def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None:
|
||||
"""
|
||||
Iterates through region data and adds locations to the multiworld if
|
||||
those locations include any of the provided tags.
|
||||
"""
|
||||
tags = set(tags)
|
||||
|
||||
for region_name, region_data in data.regions.items():
|
||||
region = regions[region_name]
|
||||
filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0]
|
||||
|
||||
for location_name in filtered_locations:
|
||||
location_data = data.locations[location_name]
|
||||
location = PokemonEmeraldLocation(
|
||||
world.player,
|
||||
location_data.label,
|
||||
location_data.flag,
|
||||
region,
|
||||
location_data.rom_address,
|
||||
location_data.default_item,
|
||||
location_data.tags
|
||||
)
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
def create_location_label_to_id_map() -> Dict[str, int]:
|
||||
"""
|
||||
Creates a map from location labels to their AP location id (address)
|
||||
"""
|
||||
label_to_id_map: Dict[str, int] = {}
|
||||
for region_data in data.regions.values():
|
||||
for location_name in region_data.locations:
|
||||
location_data = data.locations[location_name]
|
||||
label_to_id_map[location_data.label] = offset_flag(location_data.flag)
|
||||
|
||||
return label_to_id_map
|
||||
|
||||
|
||||
LOCATION_GROUPS = {
|
||||
"Badges": {
|
||||
"Rustboro Gym - Stone Badge",
|
||||
"Dewford Gym - Knuckle Badge",
|
||||
"Mauville Gym - Dynamo Badge",
|
||||
"Lavaridge Gym - Heat Badge",
|
||||
"Petalburg Gym - Balance Badge",
|
||||
"Fortree Gym - Feather Badge",
|
||||
"Mossdeep Gym - Mind Badge",
|
||||
"Sootopolis Gym - Rain Badge",
|
||||
},
|
||||
"Gym TMs": {
|
||||
"Rustboro Gym - TM39 from Roxanne",
|
||||
"Dewford Gym - TM08 from Brawly",
|
||||
"Mauville Gym - TM34 from Wattson",
|
||||
"Lavaridge Gym - TM50 from Flannery",
|
||||
"Petalburg Gym - TM42 from Norman",
|
||||
"Fortree Gym - TM40 from Winona",
|
||||
"Mossdeep Gym - TM04 from Tate and Liza",
|
||||
"Sootopolis Gym - TM03 from Juan",
|
||||
},
|
||||
"Postgame Locations": {
|
||||
"Littleroot Town - S.S. Ticket from Norman",
|
||||
"SS Tidal - Hidden Item in Lower Deck Trash Can",
|
||||
"SS Tidal - TM49 from Thief",
|
||||
"Safari Zone NE - Hidden Item North",
|
||||
"Safari Zone NE - Hidden Item East",
|
||||
"Safari Zone SE - Hidden Item in South Grass 1",
|
||||
"Safari Zone SE - Hidden Item in South Grass 2",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,606 @@
|
|||
"""
|
||||
Option definitions for Pokemon Emerald
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Type
|
||||
|
||||
from Options import Choice, DefaultOnToggle, Option, OptionSet, Range, Toggle, FreeText, PerGameCommonOptions
|
||||
|
||||
from .data import data
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Determines what your goal is to consider the game beaten
|
||||
|
||||
Champion: Become the champion and enter the hall of fame
|
||||
Steven: Defeat Steven in Meteor Falls
|
||||
Norman: Defeat Norman in Petalburg Gym
|
||||
"""
|
||||
display_name = "Goal"
|
||||
default = 0
|
||||
option_champion = 0
|
||||
option_steven = 1
|
||||
option_norman = 2
|
||||
|
||||
|
||||
class RandomizeBadges(Choice):
|
||||
"""
|
||||
Adds Badges to the pool
|
||||
|
||||
Vanilla: Gym leaders give their own badge
|
||||
Shuffle: Gym leaders give a random badge
|
||||
Completely Random: Badges can be found anywhere
|
||||
"""
|
||||
display_name = "Randomize Badges"
|
||||
default = 2
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_completely_random = 2
|
||||
|
||||
|
||||
class RandomizeHms(Choice):
|
||||
"""
|
||||
Adds HMs to the pool
|
||||
|
||||
Vanilla: HMs are at their vanilla locations
|
||||
Shuffle: HMs are shuffled among vanilla HM locations
|
||||
Completely Random: HMs can be found anywhere
|
||||
"""
|
||||
display_name = "Randomize HMs"
|
||||
default = 2
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_completely_random = 2
|
||||
|
||||
|
||||
class RandomizeKeyItems(DefaultOnToggle):
|
||||
"""
|
||||
Adds most key items to the pool. These are usually required to unlock
|
||||
a location or region (e.g. Devon Scope, Letter, Basement Key)
|
||||
"""
|
||||
display_name = "Randomize Key Items"
|
||||
|
||||
|
||||
class RandomizeBikes(Toggle):
|
||||
"""
|
||||
Adds the mach bike and acro bike to the pool
|
||||
"""
|
||||
display_name = "Randomize Bikes"
|
||||
|
||||
|
||||
class RandomizeRods(Toggle):
|
||||
"""
|
||||
Adds fishing rods to the pool
|
||||
"""
|
||||
display_name = "Randomize Fishing Rods"
|
||||
|
||||
|
||||
class RandomizeOverworldItems(DefaultOnToggle):
|
||||
"""
|
||||
Adds items on the ground with a Pokeball sprite to the pool
|
||||
"""
|
||||
display_name = "Randomize Overworld Items"
|
||||
|
||||
|
||||
class RandomizeHiddenItems(Toggle):
|
||||
"""
|
||||
Adds hidden items to the pool
|
||||
"""
|
||||
display_name = "Randomize Hidden Items"
|
||||
|
||||
|
||||
class RandomizeNpcGifts(Toggle):
|
||||
"""
|
||||
Adds most gifts received from NPCs to the pool (not including key items or HMs)
|
||||
"""
|
||||
display_name = "Randomize NPC Gifts"
|
||||
|
||||
|
||||
class ItemPoolType(Choice):
|
||||
"""
|
||||
Determines which non-progression items get put into the item pool
|
||||
|
||||
Shuffled: Item pool consists of shuffled vanilla items
|
||||
Diverse Balanced: Item pool consists of random items approximately proportioned
|
||||
according to what they're replacing (i.e. more pokeballs, fewer X items, etc...)
|
||||
Diverse: Item pool consists of uniformly random (non-unique) items
|
||||
"""
|
||||
display_name = "Item Pool Type"
|
||||
default = 0
|
||||
option_shuffled = 0
|
||||
option_diverse_balanced = 1
|
||||
option_diverse = 2
|
||||
|
||||
|
||||
class HiddenItemsRequireItemfinder(DefaultOnToggle):
|
||||
"""
|
||||
The Itemfinder is logically required to pick up hidden items
|
||||
"""
|
||||
display_name = "Require Itemfinder"
|
||||
|
||||
|
||||
class DarkCavesRequireFlash(DefaultOnToggle):
|
||||
"""
|
||||
The lower floors of Granite Cave and Victory Road logically require use of HM05 Flash
|
||||
"""
|
||||
display_name = "Require Flash"
|
||||
|
||||
|
||||
class EnableFerry(Toggle):
|
||||
"""
|
||||
The ferry between Slateport, Lilycove, and the Battle Frontier can be used if you have the S.S. Ticket
|
||||
"""
|
||||
display_name = "Enable Ferry"
|
||||
|
||||
|
||||
class EliteFourRequirement(Choice):
|
||||
"""
|
||||
Sets the requirements to challenge the elite four
|
||||
|
||||
Badges: Obtain some number of badges
|
||||
Gyms: Defeat some number of gyms
|
||||
"""
|
||||
display_name = "Elite Four Requirement"
|
||||
default = 0
|
||||
option_badges = 0
|
||||
option_gyms = 1
|
||||
|
||||
|
||||
class EliteFourCount(Range):
|
||||
"""
|
||||
Sets the number of badges/gyms required to challenge the elite four
|
||||
"""
|
||||
display_name = "Elite Four Count"
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 8
|
||||
|
||||
|
||||
class NormanRequirement(Choice):
|
||||
"""
|
||||
Sets the requirements to challenge the Petalburg Gym
|
||||
|
||||
Badges: Obtain some number of badges
|
||||
Gyms: Defeat some number of gyms
|
||||
"""
|
||||
display_name = "Norman Requirement"
|
||||
default = 0
|
||||
option_badges = 0
|
||||
option_gyms = 1
|
||||
|
||||
|
||||
class NormanCount(Range):
|
||||
"""
|
||||
Sets the number of badges/gyms required to challenge the Petalburg Gym
|
||||
"""
|
||||
display_name = "Norman Count"
|
||||
range_start = 0
|
||||
range_end = 7
|
||||
default = 4
|
||||
|
||||
|
||||
class RandomizeWildPokemon(Choice):
|
||||
"""
|
||||
Randomizes wild pokemon encounters (grass, caves, water, fishing)
|
||||
|
||||
Vanilla: Wild encounters are unchanged
|
||||
Match Base Stats: Wild pokemon are replaced with species with approximately the same bst
|
||||
Match Type: Wild pokemon are replaced with species that share a type with the original
|
||||
Match Base Stats and Type: Apply both Match Base Stats and Match Type
|
||||
Completely Random: There are no restrictions
|
||||
"""
|
||||
display_name = "Randomize Wild Pokemon"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_base_stats = 1
|
||||
option_match_type = 2
|
||||
option_match_base_stats_and_type = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class AllowWildLegendaries(DefaultOnToggle):
|
||||
"""
|
||||
Wild encounters can be replaced by legendaries. Only applied if Randomize Wild Pokemon is not Vanilla.
|
||||
"""
|
||||
display_name = "Allow Wild Legendaries"
|
||||
|
||||
|
||||
class RandomizeStarters(Choice):
|
||||
"""
|
||||
Randomizes the starter pokemon in Professor Birch's bag
|
||||
|
||||
Vanilla: Starters are unchanged
|
||||
Match Base Stats: Starters are replaced with species with approximately the same bst
|
||||
Match Type: Starters are replaced with species that share a type with the original
|
||||
Match Base Stats and Type: Apply both Match Base Stats and Match Type
|
||||
Completely Random: There are no restrictions
|
||||
"""
|
||||
display_name = "Randomize Starters"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_base_stats = 1
|
||||
option_match_type = 2
|
||||
option_match_base_stats_and_type = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class AllowStarterLegendaries(DefaultOnToggle):
|
||||
"""
|
||||
Starters can be replaced by legendaries. Only applied if Randomize Starters is not Vanilla.
|
||||
"""
|
||||
display_name = "Allow Starter Legendaries"
|
||||
|
||||
|
||||
class RandomizeTrainerParties(Choice):
|
||||
"""
|
||||
Randomizes the parties of all trainers.
|
||||
|
||||
Vanilla: Parties are unchanged
|
||||
Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst
|
||||
Match Type: Trainer pokemon are replaced with species that share a type with the original
|
||||
Match Base Stats and Type: Apply both Match Base Stats and Match Type
|
||||
Completely Random: There are no restrictions
|
||||
"""
|
||||
display_name = "Randomize Trainer Parties"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_base_stats = 1
|
||||
option_match_type = 2
|
||||
option_match_base_stats_and_type = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class AllowTrainerLegendaries(DefaultOnToggle):
|
||||
"""
|
||||
Enemy trainer pokemon can be replaced by legendaries. Only applied if Randomize Trainer Parties is not Vanilla.
|
||||
"""
|
||||
display_name = "Allow Trainer Legendaries"
|
||||
|
||||
|
||||
class RandomizeStaticEncounters(Choice):
|
||||
"""
|
||||
Randomizes static encounters (Rayquaza, hidden Kekleons, fake Voltorb pokeballs, etc...)
|
||||
|
||||
Vanilla: Static encounters are unchanged
|
||||
Shuffle: Static encounters are shuffled between each other
|
||||
Match Base Stats: Static encounters are replaced with species with approximately the same bst
|
||||
Match Type: Static encounters are replaced with species that share a type with the original
|
||||
Match Base Stats and Type: Apply both Match Base Stats and Match Type
|
||||
Completely Random: There are no restrictions
|
||||
"""
|
||||
display_name = "Randomize Static Encounters"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_match_base_stats = 2
|
||||
option_match_type = 3
|
||||
option_match_base_stats_and_type = 4
|
||||
option_completely_random = 5
|
||||
|
||||
|
||||
class RandomizeTypes(Choice):
|
||||
"""
|
||||
Randomizes the type(s) of every pokemon. Each species will have the same number of types.
|
||||
|
||||
Vanilla: Types are unchanged
|
||||
Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type)
|
||||
Completely Random: Each species has its type(s) randomized
|
||||
Follow Evolutions: Types are randomized per evolution line instead of per species
|
||||
"""
|
||||
display_name = "Randomize Types"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_completely_random = 2
|
||||
option_follow_evolutions = 3
|
||||
|
||||
|
||||
class RandomizeAbilities(Choice):
|
||||
"""
|
||||
Randomizes abilities of every species. Each species will have the same number of abilities.
|
||||
|
||||
Vanilla: Abilities are unchanged
|
||||
Completely Random: Each species has its abilities randomized
|
||||
Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability
|
||||
when evolving, the random ability will also be retained
|
||||
"""
|
||||
display_name = "Randomize Abilities"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_completely_random = 1
|
||||
option_follow_evolutions = 2
|
||||
|
||||
|
||||
class AbilityBlacklist(OptionSet):
|
||||
"""
|
||||
A list of abilities which no pokemon should have if abilities are randomized.
|
||||
For example, you could exclude Wonder Guard and Arena Trap like this:
|
||||
["Wonder Guard", "Arena Trap"]
|
||||
"""
|
||||
display_name = "Ability Blacklist"
|
||||
valid_keys = frozenset([ability.label for ability in data.abilities])
|
||||
|
||||
|
||||
class LevelUpMoves(Choice):
|
||||
"""
|
||||
Randomizes the moves a pokemon learns when they reach a level where they would learn a move.
|
||||
Your starter is guaranteed to have a usable damaging move.
|
||||
|
||||
Vanilla: Learnset is unchanged
|
||||
Randomized: Moves are randomized
|
||||
Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1
|
||||
"""
|
||||
display_name = "Level Up Moves"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_randomized = 1
|
||||
option_start_with_four_moves = 2
|
||||
|
||||
|
||||
class MoveMatchTypeBias(Range):
|
||||
"""
|
||||
Sets the probability that a learned move will be forced match one of the types of a pokemon.
|
||||
|
||||
If a move is not forced to match type, it will roll for Normal type bias.
|
||||
"""
|
||||
display_name = "Move Match Type Bias"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class MoveNormalTypeBias(Range):
|
||||
"""
|
||||
After it has been decided that a move will not be forced to match types, sets the probability that a learned move
|
||||
will be forced to be the Normal type.
|
||||
|
||||
If a move is not forced to be Normal, it will be completely random.
|
||||
"""
|
||||
display_name = "Move Normal Type Bias"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class HmCompatibility(Choice):
|
||||
"""
|
||||
Modifies the compatibility of HMs
|
||||
|
||||
Vanilla: Compatibility is unchanged
|
||||
Fully Compatible: Every species can learn any HM
|
||||
Completely Random: Compatibility is 50/50 for every HM (does not remain consistent across evolution)
|
||||
"""
|
||||
display_name = "HM Compatibility"
|
||||
default = 1
|
||||
option_vanilla = 0
|
||||
option_fully_compatible = 1
|
||||
option_completely_random = 2
|
||||
|
||||
|
||||
class TmCompatibility(Choice):
|
||||
"""
|
||||
Modifies the compatibility of TMs
|
||||
|
||||
Vanilla: Compatibility is unchanged
|
||||
Fully Compatible: Every species can learn any TM
|
||||
Completely Random: Compatibility is 50/50 for every TM (does not remain consistent across evolution)
|
||||
"""
|
||||
display_name = "TM Compatibility"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_fully_compatible = 1
|
||||
option_completely_random = 2
|
||||
|
||||
|
||||
class TmMoves(Toggle):
|
||||
"""
|
||||
Randomizes the moves taught by TMs
|
||||
"""
|
||||
display_name = "TM Moves"
|
||||
|
||||
|
||||
class ReusableTms(Toggle):
|
||||
"""
|
||||
Sets TMs to not break after use (they remain sellable)
|
||||
"""
|
||||
display_name = "Reusable TMs"
|
||||
|
||||
|
||||
class MinCatchRate(Range):
|
||||
"""
|
||||
Sets the minimum catch rate a pokemon can have. Any pokemon with a catch rate below this floor will have it raised to this value.
|
||||
|
||||
Legendaries are often in the single digits
|
||||
Fully evolved pokemon are often double digits
|
||||
Pidgey is 255
|
||||
"""
|
||||
display_name = "Minimum Catch Rate"
|
||||
range_start = 3
|
||||
range_end = 255
|
||||
default = 3
|
||||
|
||||
|
||||
class GuaranteedCatch(Toggle):
|
||||
"""
|
||||
Every throw is guaranteed to catch a wild pokemon
|
||||
"""
|
||||
display_name = "Guaranteed Catch"
|
||||
|
||||
|
||||
class ExpModifier(Range):
|
||||
"""
|
||||
Multiplies gained experience by a percentage
|
||||
|
||||
100 is default
|
||||
50 is half
|
||||
200 is double
|
||||
etc...
|
||||
"""
|
||||
display_name = "Exp Modifier"
|
||||
range_start = 0
|
||||
range_end = 1000
|
||||
default = 100
|
||||
|
||||
|
||||
class BlindTrainers(Toggle):
|
||||
"""
|
||||
Causes trainers to not start a battle with you unless you talk to them
|
||||
"""
|
||||
display_name = "Blind Trainers"
|
||||
|
||||
|
||||
class DoubleBattleChance(Range):
|
||||
"""
|
||||
The percent chance that a trainer with more than 1 pokemon will be converted into a double battle.
|
||||
If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon.
|
||||
They can be battled by talking to them no matter what.
|
||||
"""
|
||||
display_name = "Double Battle Chance"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class BetterShops(Toggle):
|
||||
"""
|
||||
Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city)
|
||||
"""
|
||||
display_name = "Better Shops"
|
||||
|
||||
|
||||
class RemoveRoadblocks(OptionSet):
|
||||
"""
|
||||
Removes specific NPCs that normally stand in your way until certain events are completed.
|
||||
|
||||
This can open up the world a bit and make your playthrough less linear, but careful how many you remove; it may make too much of your world accessible upon receiving Surf.
|
||||
|
||||
Possible values are:
|
||||
"Route 110 Aqua Grunts"
|
||||
"Route 112 Magma Grunts"
|
||||
"Route 119 Aqua Grunts"
|
||||
"Safari Zone Construction Workers"
|
||||
"Lilycove City Wailmer"
|
||||
"Aqua Hideout Grunts"
|
||||
"Seafloor Cavern Aqua Grunt"
|
||||
"""
|
||||
display_name = "Remove Roadblocks"
|
||||
valid_keys = frozenset([
|
||||
"Route 110 Aqua Grunts",
|
||||
"Route 112 Magma Grunts",
|
||||
"Route 119 Aqua Grunts",
|
||||
"Safari Zone Construction Workers",
|
||||
"Lilycove City Wailmer",
|
||||
"Aqua Hideout Grunts",
|
||||
"Seafloor Cavern Aqua Grunt"
|
||||
])
|
||||
|
||||
|
||||
class ExtraBoulders(Toggle):
|
||||
"""
|
||||
Places strength boulders on Route 115 which block access to Meteor Falls from the beach.
|
||||
This aims to take some power away from Surf as a tool for access.
|
||||
"""
|
||||
display_name = "Extra Boulders"
|
||||
|
||||
|
||||
class FreeFlyLocation(Toggle):
|
||||
"""
|
||||
Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items)
|
||||
"""
|
||||
display_name = "Free Fly Location"
|
||||
|
||||
|
||||
class FlyWithoutBadge(DefaultOnToggle):
|
||||
"""
|
||||
Fly does not require the Feather Badge to use in the field
|
||||
"""
|
||||
display_name = "Fly Without Badge"
|
||||
|
||||
|
||||
class TurboA(Toggle):
|
||||
"""
|
||||
Holding A will advance most text automatically
|
||||
"""
|
||||
display_name = "Turbo A"
|
||||
|
||||
|
||||
class ReceiveItemMessages(Choice):
|
||||
"""
|
||||
Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld.
|
||||
|
||||
All: Every item shows a message
|
||||
Progression: Only progression items show a message
|
||||
None: All items are added to your bag silently (badges will still show)
|
||||
"""
|
||||
display_name = "Receive Item Messages"
|
||||
default = 0
|
||||
option_all = 0
|
||||
option_progression = 1
|
||||
option_none = 2
|
||||
|
||||
|
||||
class EasterEgg(FreeText):
|
||||
"""
|
||||
???
|
||||
"""
|
||||
default = "Example Passphrase"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PokemonEmeraldOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
|
||||
badges: RandomizeBadges
|
||||
hms: RandomizeHms
|
||||
key_items: RandomizeKeyItems
|
||||
bikes: RandomizeBikes
|
||||
rods: RandomizeRods
|
||||
overworld_items: RandomizeOverworldItems
|
||||
hidden_items: RandomizeHiddenItems
|
||||
npc_gifts: RandomizeNpcGifts
|
||||
item_pool_type: ItemPoolType
|
||||
|
||||
require_itemfinder: HiddenItemsRequireItemfinder
|
||||
require_flash: DarkCavesRequireFlash
|
||||
elite_four_requirement: EliteFourRequirement
|
||||
elite_four_count: EliteFourCount
|
||||
norman_requirement: NormanRequirement
|
||||
norman_count: NormanCount
|
||||
|
||||
wild_pokemon: RandomizeWildPokemon
|
||||
allow_wild_legendaries: AllowWildLegendaries
|
||||
starters: RandomizeStarters
|
||||
allow_starter_legendaries: AllowStarterLegendaries
|
||||
trainer_parties: RandomizeTrainerParties
|
||||
allow_trainer_legendaries: AllowTrainerLegendaries
|
||||
static_encounters: RandomizeStaticEncounters
|
||||
types: RandomizeTypes
|
||||
abilities: RandomizeAbilities
|
||||
ability_blacklist: AbilityBlacklist
|
||||
|
||||
level_up_moves: LevelUpMoves
|
||||
move_match_type_bias: MoveMatchTypeBias
|
||||
move_normal_type_bias: MoveNormalTypeBias
|
||||
tm_compatibility: TmCompatibility
|
||||
hm_compatibility: HmCompatibility
|
||||
tm_moves: TmMoves
|
||||
reusable_tms: ReusableTms
|
||||
|
||||
min_catch_rate: MinCatchRate
|
||||
guaranteed_catch: GuaranteedCatch
|
||||
exp_modifier: ExpModifier
|
||||
blind_trainers: BlindTrainers
|
||||
double_battle_chance: DoubleBattleChance
|
||||
better_shops: BetterShops
|
||||
|
||||
enable_ferry: EnableFerry
|
||||
remove_roadblocks: RemoveRoadblocks
|
||||
extra_boulders: ExtraBoulders
|
||||
free_fly_location: FreeFlyLocation
|
||||
fly_without_badge: FlyWithoutBadge
|
||||
|
||||
turbo_a: TurboA
|
||||
receive_item_messages: ReceiveItemMessages
|
||||
|
||||
easter_egg: EasterEgg
|
|
@ -0,0 +1,196 @@
|
|||
"""
|
||||
Functions related to pokemon species and moves
|
||||
"""
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
|
||||
|
||||
from .data import SpeciesData, data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from random import Random
|
||||
|
||||
|
||||
_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
|
||||
})
|
||||
|
||||
_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
|
||||
]
|
||||
|
||||
_moves_by_type: Dict[int, List[int]] = {}
|
||||
for move, type in enumerate(_move_types):
|
||||
_moves_by_type.setdefault(type, []).append(move)
|
||||
|
||||
_move_blacklist = frozenset({
|
||||
0, # MOVE_NONE
|
||||
165, # Struggle
|
||||
15, # Cut
|
||||
148, # Flash
|
||||
249, # Rock Smash
|
||||
70, # Strength
|
||||
57, # Surf
|
||||
19, # Fly
|
||||
291, # Dive
|
||||
127 # Waterfall
|
||||
})
|
||||
|
||||
_legendary_pokemon = frozenset({
|
||||
'Mew',
|
||||
'Mewtwo',
|
||||
'Articuno',
|
||||
'Zapdos',
|
||||
'Moltres',
|
||||
'Lugia',
|
||||
'Ho-oh',
|
||||
'Raikou',
|
||||
'Suicune',
|
||||
'Entei',
|
||||
'Celebi',
|
||||
'Groudon',
|
||||
'Kyogre',
|
||||
'Rayquaza',
|
||||
'Latios',
|
||||
'Latias',
|
||||
'Registeel',
|
||||
'Regirock',
|
||||
'Regice',
|
||||
'Jirachi',
|
||||
'Deoxys'
|
||||
})
|
||||
|
||||
|
||||
def get_random_species(
|
||||
random: "Random",
|
||||
candidates: List[Optional[SpeciesData]],
|
||||
nearby_bst: Optional[int] = None,
|
||||
species_type: Optional[int] = None,
|
||||
allow_legendaries: bool = True) -> SpeciesData:
|
||||
candidates: List[SpeciesData] = [species for species in candidates if species is not None]
|
||||
|
||||
if species_type is not None:
|
||||
candidates = [species for species in candidates if species_type in species.types]
|
||||
|
||||
if not allow_legendaries:
|
||||
candidates = [species for species in candidates if species.label not in _legendary_pokemon]
|
||||
|
||||
if nearby_bst is not None:
|
||||
def has_nearby_bst(species: SpeciesData, max_percent_different: int) -> bool:
|
||||
return abs(sum(species.base_stats) - nearby_bst) < nearby_bst * (max_percent_different / 100)
|
||||
|
||||
max_percent_different = 10
|
||||
bst_filtered_candidates = [species for species in candidates if has_nearby_bst(species, max_percent_different)]
|
||||
while len(bst_filtered_candidates) == 0:
|
||||
max_percent_different += 10
|
||||
bst_filtered_candidates = [
|
||||
species
|
||||
for species in candidates
|
||||
if has_nearby_bst(species, max_percent_different)
|
||||
]
|
||||
|
||||
candidates = bst_filtered_candidates
|
||||
|
||||
return random.choice(candidates)
|
||||
|
||||
|
||||
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["MOVE_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
|
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
Functions related to AP regions for Pokemon Emerald (see ./data/regions for region definitions)
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
|
||||
from BaseClasses import ItemClassification, Region
|
||||
|
||||
from .data import data
|
||||
from .items import PokemonEmeraldItem
|
||||
from .locations import PokemonEmeraldLocation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import PokemonEmeraldWorld
|
||||
|
||||
|
||||
def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
|
||||
"""
|
||||
Iterates through regions created from JSON to create regions and adds them to the multiworld.
|
||||
Also creates and places events and connects regions via warps and the exits defined in the JSON.
|
||||
"""
|
||||
regions: Dict[str, Region] = {}
|
||||
connections: List[Tuple[str, str, str]] = []
|
||||
|
||||
for region_name, region_data in data.regions.items():
|
||||
new_region = Region(region_name, world.player, world.multiworld)
|
||||
|
||||
for event_data in region_data.events:
|
||||
event = PokemonEmeraldLocation(world.player, event_data.name, None, new_region)
|
||||
event.place_locked_item(PokemonEmeraldItem(event_data.name, ItemClassification.progression, None, world.player))
|
||||
new_region.locations.append(event)
|
||||
|
||||
for region_exit in region_data.exits:
|
||||
connections.append((f"{region_name} -> {region_exit}", region_name, region_exit))
|
||||
|
||||
for warp in region_data.warps:
|
||||
dest_warp = data.warps[data.warp_map[warp]]
|
||||
if dest_warp.parent_region is None:
|
||||
continue
|
||||
connections.append((warp, region_name, dest_warp.parent_region))
|
||||
|
||||
regions[region_name] = new_region
|
||||
|
||||
for name, source, dest in connections:
|
||||
regions[source].connect(regions[dest], name)
|
||||
|
||||
regions["Menu"] = Region("Menu", world.player, world.multiworld)
|
||||
regions["Menu"].connect(regions["REGION_LITTLEROOT_TOWN/MAIN"], "Start Game")
|
||||
|
||||
return regions
|
|
@ -0,0 +1,420 @@
|
|||
"""
|
||||
Classes and functions related to creating a ROM patch
|
||||
"""
|
||||
import os
|
||||
import pkgutil
|
||||
from typing import TYPE_CHECKING, List, Tuple
|
||||
|
||||
import bsdiff4
|
||||
|
||||
from worlds.Files import APDeltaPatch
|
||||
from settings import get_settings
|
||||
|
||||
from .data import PokemonEmeraldData, TrainerPokemonDataTypeEnum, data
|
||||
from .items import reverse_offset_item_value
|
||||
from .options import RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement
|
||||
from .pokemon import get_random_species
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import PokemonEmeraldWorld
|
||||
|
||||
|
||||
class PokemonEmeraldDeltaPatch(APDeltaPatch):
|
||||
game = "Pokemon Emerald"
|
||||
hash = "605b89b67018abcea91e693a4dd25be3"
|
||||
patch_file_ending = ".apemerald"
|
||||
result_file_ending = ".gba"
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_as_bytes()
|
||||
|
||||
|
||||
location_visited_event_to_id_map = {
|
||||
"EVENT_VISITED_LITTLEROOT_TOWN": 0,
|
||||
"EVENT_VISITED_OLDALE_TOWN": 1,
|
||||
"EVENT_VISITED_PETALBURG_CITY": 2,
|
||||
"EVENT_VISITED_RUSTBORO_CITY": 3,
|
||||
"EVENT_VISITED_DEWFORD_TOWN": 4,
|
||||
"EVENT_VISITED_SLATEPORT_CITY": 5,
|
||||
"EVENT_VISITED_MAUVILLE_CITY": 6,
|
||||
"EVENT_VISITED_VERDANTURF_TOWN": 7,
|
||||
"EVENT_VISITED_FALLARBOR_TOWN": 8,
|
||||
"EVENT_VISITED_LAVARIDGE_TOWN": 9,
|
||||
"EVENT_VISITED_FORTREE_CITY": 10,
|
||||
"EVENT_VISITED_LILYCOVE_CITY": 11,
|
||||
"EVENT_VISITED_MOSSDEEP_CITY": 12,
|
||||
"EVENT_VISITED_SOOTOPOLIS_CITY": 13,
|
||||
"EVENT_VISITED_PACIFIDLOG_TOWN": 14,
|
||||
"EVENT_VISITED_EVER_GRANDE_CITY": 15,
|
||||
"EVENT_VISITED_BATTLE_FRONTIER": 16,
|
||||
"EVENT_VISITED_SOUTHERN_ISLAND": 17
|
||||
}
|
||||
|
||||
|
||||
def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None:
|
||||
base_rom = get_base_rom_as_bytes()
|
||||
base_patch = pkgutil.get_data(__name__, "data/base_patch.bsdiff4")
|
||||
patched_rom = bytearray(bsdiff4.patch(base_rom, base_patch))
|
||||
|
||||
# Set item values
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
# Set free fly location
|
||||
if location.address is None:
|
||||
if world.options.free_fly_location and location.name == "EVENT_VISITED_LITTLEROOT_TOWN":
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
data.rom_addresses["gArchipelagoOptions"] + 0x16,
|
||||
1,
|
||||
world.free_fly_location_id
|
||||
)
|
||||
continue
|
||||
|
||||
if location.item and location.item.player == world.player:
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
location.rom_address,
|
||||
2,
|
||||
reverse_offset_item_value(location.item.code)
|
||||
)
|
||||
else:
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
location.rom_address,
|
||||
2,
|
||||
data.constants["ITEM_ARCHIPELAGO_PROGRESSION"]
|
||||
)
|
||||
|
||||
# Set start inventory
|
||||
start_inventory = world.options.start_inventory.value.copy()
|
||||
|
||||
starting_badges = 0
|
||||
if start_inventory.pop("Stone Badge", 0) > 0:
|
||||
starting_badges |= (1 << 0)
|
||||
if start_inventory.pop("Knuckle Badge", 0) > 0:
|
||||
starting_badges |= (1 << 1)
|
||||
if start_inventory.pop("Dynamo Badge", 0) > 0:
|
||||
starting_badges |= (1 << 2)
|
||||
if start_inventory.pop("Heat Badge", 0) > 0:
|
||||
starting_badges |= (1 << 3)
|
||||
if start_inventory.pop("Balance Badge", 0) > 0:
|
||||
starting_badges |= (1 << 4)
|
||||
if start_inventory.pop("Feather Badge", 0) > 0:
|
||||
starting_badges |= (1 << 5)
|
||||
if start_inventory.pop("Mind Badge", 0) > 0:
|
||||
starting_badges |= (1 << 6)
|
||||
if start_inventory.pop("Rain Badge", 0) > 0:
|
||||
starting_badges |= (1 << 7)
|
||||
|
||||
pc_slots: List[Tuple[str, int]] = []
|
||||
while any(qty > 0 for qty in start_inventory.values()):
|
||||
if len(pc_slots) >= 19:
|
||||
break
|
||||
|
||||
for i, item_name in enumerate(start_inventory.keys()):
|
||||
if len(pc_slots) >= 19:
|
||||
break
|
||||
|
||||
quantity = min(start_inventory[item_name], 999)
|
||||
if quantity == 0:
|
||||
continue
|
||||
|
||||
start_inventory[item_name] -= quantity
|
||||
|
||||
pc_slots.append((item_name, quantity))
|
||||
|
||||
pc_slots.sort(reverse=True)
|
||||
|
||||
for i, slot in enumerate(pc_slots):
|
||||
address = data.rom_addresses["sNewGamePCItems"] + (i * 4)
|
||||
item = reverse_offset_item_value(world.item_name_to_id[slot[0]])
|
||||
_set_bytes_little_endian(patched_rom, address + 0, 2, item)
|
||||
_set_bytes_little_endian(patched_rom, address + 2, 2, slot[1])
|
||||
|
||||
# Set species data
|
||||
_set_species_info(world, patched_rom)
|
||||
|
||||
# Set encounter tables
|
||||
if world.options.wild_pokemon != RandomizeWildPokemon.option_vanilla:
|
||||
_set_encounter_tables(world, patched_rom)
|
||||
|
||||
# Set opponent data
|
||||
if world.options.trainer_parties != RandomizeTrainerParties.option_vanilla:
|
||||
_set_opponents(world, patched_rom)
|
||||
|
||||
# Set static pokemon
|
||||
_set_static_encounters(world, patched_rom)
|
||||
|
||||
# Set starters
|
||||
_set_starters(world, patched_rom)
|
||||
|
||||
# Set TM moves
|
||||
_set_tm_moves(world, patched_rom)
|
||||
|
||||
# Set TM/HM compatibility
|
||||
_set_tmhm_compatibility(world, patched_rom)
|
||||
|
||||
# Randomize opponent double or single
|
||||
_randomize_opponent_battle_type(world, patched_rom)
|
||||
|
||||
# Options
|
||||
# struct ArchipelagoOptions
|
||||
# {
|
||||
# /* 0x00 */ bool8 advanceTextWithHoldA;
|
||||
# /* 0x01 */ bool8 isFerryEnabled;
|
||||
# /* 0x02 */ bool8 areTrainersBlind;
|
||||
# /* 0x03 */ bool8 canFlyWithoutBadge;
|
||||
# /* 0x04 */ u16 expMultiplierNumerator;
|
||||
# /* 0x06 */ u16 expMultiplierDenominator;
|
||||
# /* 0x08 */ u16 birchPokemon;
|
||||
# /* 0x0A */ bool8 guaranteedCatch;
|
||||
# /* 0x0B */ bool8 betterShopsEnabled;
|
||||
# /* 0x0C */ bool8 eliteFourRequiresGyms;
|
||||
# /* 0x0D */ u8 eliteFourRequiredCount;
|
||||
# /* 0x0E */ bool8 normanRequiresGyms;
|
||||
# /* 0x0F */ u8 normanRequiredCount;
|
||||
# /* 0x10 */ u8 startingBadges;
|
||||
# /* 0x11 */ u8 receivedItemMessageFilter; // 0 = Show All; 1 = Show Progression Only; 2 = Show None
|
||||
# /* 0x12 */ bool8 reusableTms;
|
||||
# /* 0x14 */ u16 removedBlockers;
|
||||
# /* 0x13 */ bool8 addRoute115Boulders;
|
||||
# /* 0x14 */ u16 removedBlockers;
|
||||
# /* 0x14 */ u16 removedBlockers;
|
||||
# /* 0x16 */ u8 freeFlyLocation;
|
||||
# };
|
||||
options_address = data.rom_addresses["gArchipelagoOptions"]
|
||||
|
||||
# Set hold A to advance text
|
||||
turbo_a = 1 if world.options.turbo_a else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x00, 1, turbo_a)
|
||||
|
||||
# Set ferry enabled
|
||||
enable_ferry = 1 if world.options.enable_ferry else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x01, 1, enable_ferry)
|
||||
|
||||
# Set blind trainers
|
||||
blind_trainers = 1 if world.options.blind_trainers else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x02, 1, blind_trainers)
|
||||
|
||||
# Set fly without badge
|
||||
fly_without_badge = 1 if world.options.fly_without_badge else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x03, 1, fly_without_badge)
|
||||
|
||||
# Set exp modifier
|
||||
numerator = min(max(world.options.exp_modifier.value, 0), 2**16 - 1)
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x04, 2, numerator)
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x06, 2, 100)
|
||||
|
||||
# Set Birch pokemon
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
options_address + 0x08,
|
||||
2,
|
||||
get_random_species(world.random, data.species).species_id
|
||||
)
|
||||
|
||||
# Set guaranteed catch
|
||||
guaranteed_catch = 1 if world.options.guaranteed_catch else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0A, 1, guaranteed_catch)
|
||||
|
||||
# Set better shops
|
||||
better_shops = 1 if world.options.better_shops else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0B, 1, better_shops)
|
||||
|
||||
# Set elite four requirement
|
||||
elite_four_requires_gyms = 1 if world.options.elite_four_requirement == EliteFourRequirement.option_gyms else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0C, 1, elite_four_requires_gyms)
|
||||
|
||||
# Set elite four count
|
||||
elite_four_count = min(max(world.options.elite_four_count.value, 0), 8)
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0D, 1, elite_four_count)
|
||||
|
||||
# Set norman requirement
|
||||
norman_requires_gyms = 1 if world.options.norman_requirement == NormanRequirement.option_gyms else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0E, 1, norman_requires_gyms)
|
||||
|
||||
# Set norman count
|
||||
norman_count = min(max(world.options.norman_count.value, 0), 8)
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x0F, 1, norman_count)
|
||||
|
||||
# Set starting badges
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x10, 1, starting_badges)
|
||||
|
||||
# Set receive item messages type
|
||||
receive_item_messages_type = world.options.receive_item_messages.value
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x11, 1, receive_item_messages_type)
|
||||
|
||||
# Set reusable TMs
|
||||
reusable_tms = 1 if world.options.reusable_tms else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x12, 1, reusable_tms)
|
||||
|
||||
# Set route 115 boulders
|
||||
route_115_boulders = 1 if world.options.extra_boulders else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x13, 1, route_115_boulders)
|
||||
|
||||
# Set removed blockers
|
||||
removed_roadblocks = world.options.remove_roadblocks.value
|
||||
removed_roadblocks_bitfield = 0
|
||||
removed_roadblocks_bitfield |= (1 << 0) if "Safari Zone Construction Workers" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 1) if "Lilycove City Wailmer" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 2) if "Route 110 Aqua Grunts" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 3) if "Aqua Hideout Grunts" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 4) if "Route 119 Aqua Grunts" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 5) if "Route 112 Magma Grunts" in removed_roadblocks else 0
|
||||
removed_roadblocks_bitfield |= (1 << 6) if "Seafloor Cavern Aqua Grunt" in removed_roadblocks else 0
|
||||
_set_bytes_little_endian(patched_rom, options_address + 0x14, 2, removed_roadblocks_bitfield)
|
||||
|
||||
# Set slot name
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
for i, byte in enumerate(player_name.encode("utf-8")):
|
||||
_set_bytes_little_endian(patched_rom, data.rom_addresses["gArchipelagoInfo"] + i, 1, byte)
|
||||
|
||||
# Write Output
|
||||
out_file_name = world.multiworld.get_out_file_name_base(world.player)
|
||||
output_path = os.path.join(output_directory, f"{out_file_name}.gba")
|
||||
with open(output_path, "wb") as out_file:
|
||||
out_file.write(patched_rom)
|
||||
patch = PokemonEmeraldDeltaPatch(os.path.splitext(output_path)[0] + ".apemerald", player=world.player,
|
||||
player_name=player_name, patched_path=output_path)
|
||||
|
||||
patch.write()
|
||||
os.unlink(output_path)
|
||||
|
||||
|
||||
def get_base_rom_as_bytes() -> bytes:
|
||||
with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile:
|
||||
base_rom_bytes = bytes(infile.read())
|
||||
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def _set_bytes_little_endian(byte_array: bytearray, address: int, size: int, value: int) -> None:
|
||||
offset = 0
|
||||
while size > 0:
|
||||
byte_array[address + offset] = value & 0xFF
|
||||
value = value >> 8
|
||||
offset += 1
|
||||
size -= 1
|
||||
|
||||
|
||||
def _set_encounter_tables(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
"""
|
||||
Encounter tables are lists of
|
||||
struct {
|
||||
min_level: 0x01 bytes,
|
||||
max_level: 0x01 bytes,
|
||||
species_id: 0x02 bytes
|
||||
}
|
||||
"""
|
||||
|
||||
for map_data in world.modified_maps:
|
||||
tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
|
||||
for table in tables:
|
||||
if table is not None:
|
||||
for i, species_id in enumerate(table.slots):
|
||||
address = table.rom_address + 2 + (4 * i)
|
||||
_set_bytes_little_endian(rom, address, 2, species_id)
|
||||
|
||||
|
||||
def _set_species_info(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
for species in world.modified_species:
|
||||
if species is not None:
|
||||
_set_bytes_little_endian(rom, species.rom_address + 6, 1, species.types[0])
|
||||
_set_bytes_little_endian(rom, species.rom_address + 7, 1, species.types[1])
|
||||
_set_bytes_little_endian(rom, species.rom_address + 8, 1, species.catch_rate)
|
||||
_set_bytes_little_endian(rom, species.rom_address + 22, 1, species.abilities[0])
|
||||
_set_bytes_little_endian(rom, species.rom_address + 23, 1, species.abilities[1])
|
||||
|
||||
for i, learnset_move in enumerate(species.learnset):
|
||||
level_move = learnset_move.level << 9 | learnset_move.move_id
|
||||
_set_bytes_little_endian(rom, species.learnset_rom_address + (i * 2), 2, level_move)
|
||||
|
||||
|
||||
def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
for trainer in world.modified_trainers:
|
||||
party_address = trainer.party.rom_address
|
||||
|
||||
pokemon_data_size: int
|
||||
if trainer.party.pokemon_data_type in {TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES, TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES}:
|
||||
pokemon_data_size = 8
|
||||
else: # Custom Moves
|
||||
pokemon_data_size = 16
|
||||
|
||||
for i, pokemon in enumerate(trainer.party.pokemon):
|
||||
pokemon_address = party_address + (i * pokemon_data_size)
|
||||
|
||||
# Replace species
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x04, 2, pokemon.species_id)
|
||||
|
||||
# Replace custom moves if applicable
|
||||
if trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES:
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x06, 2, pokemon.moves[0])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x08, 2, pokemon.moves[1])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x0A, 2, pokemon.moves[2])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x0C, 2, pokemon.moves[3])
|
||||
elif trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES:
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x08, 2, pokemon.moves[0])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x0A, 2, pokemon.moves[1])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x0C, 2, pokemon.moves[2])
|
||||
_set_bytes_little_endian(rom, pokemon_address + 0x0E, 2, pokemon.moves[3])
|
||||
|
||||
|
||||
def _set_static_encounters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
for encounter in world.modified_static_encounters:
|
||||
_set_bytes_little_endian(rom, encounter.rom_address, 2, encounter.species_id)
|
||||
|
||||
|
||||
def _set_starters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
address = data.rom_addresses["sStarterMon"]
|
||||
(starter_1, starter_2, starter_3) = world.modified_starters
|
||||
|
||||
_set_bytes_little_endian(rom, address + 0, 2, starter_1)
|
||||
_set_bytes_little_endian(rom, address + 2, 2, starter_2)
|
||||
_set_bytes_little_endian(rom, address + 4, 2, starter_3)
|
||||
|
||||
|
||||
def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
tmhm_list_address = data.rom_addresses["sTMHMMoves"]
|
||||
|
||||
for i, move in enumerate(world.modified_tmhm_moves):
|
||||
# Don't modify HMs
|
||||
if i >= 50:
|
||||
break
|
||||
|
||||
_set_bytes_little_endian(rom, tmhm_list_address + (i * 2), 2, move)
|
||||
|
||||
|
||||
def _set_tmhm_compatibility(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
learnsets_address = data.rom_addresses["gTMHMLearnsets"]
|
||||
|
||||
for species in world.modified_species:
|
||||
if species is not None:
|
||||
_set_bytes_little_endian(rom, learnsets_address + (species.species_id * 8), 8, species.tm_hm_compatibility)
|
||||
|
||||
|
||||
def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
|
||||
probability = world.options.double_battle_chance.value / 100
|
||||
|
||||
battle_type_map = {
|
||||
0: 4,
|
||||
1: 8,
|
||||
2: 6,
|
||||
3: 13,
|
||||
}
|
||||
|
||||
for trainer_data in data.trainers:
|
||||
if trainer_data.battle_script_rom_address != 0 and len(trainer_data.party.pokemon) > 1:
|
||||
if world.random.random() < probability:
|
||||
# Set the trainer to be a double battle
|
||||
_set_bytes_little_endian(rom, trainer_data.rom_address + 0x18, 1, 1)
|
||||
|
||||
# Swap the battle type in the script for the purpose of loading the right text
|
||||
# and setting data to the right places
|
||||
original_battle_type = rom[trainer_data.battle_script_rom_address + 1]
|
||||
if original_battle_type in battle_type_map:
|
||||
_set_bytes_little_endian(
|
||||
rom,
|
||||
trainer_data.battle_script_rom_address + 1,
|
||||
1,
|
||||
battle_type_map[original_battle_type]
|
||||
)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,352 @@
|
|||
"""
|
||||
Looks through data object to double-check it makes sense. Will fail for missing or duplicate definitions or
|
||||
duplicate claims and give warnings for unused and unignored locations or warps.
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from .data import data
|
||||
|
||||
|
||||
_ignorable_locations = {
|
||||
# Trick House
|
||||
"HIDDEN_ITEM_TRICK_HOUSE_NUGGET",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL",
|
||||
"ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL",
|
||||
|
||||
# Battle Frontier
|
||||
"ITEM_ARTISAN_CAVE_1F_CARBOS",
|
||||
"ITEM_ARTISAN_CAVE_B1F_HP_UP",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN",
|
||||
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC",
|
||||
|
||||
# Event islands
|
||||
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
|
||||
}
|
||||
|
||||
_ignorable_warps = {
|
||||
# Trick House
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
|
||||
|
||||
# Department store elevator
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:3/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
|
||||
|
||||
# Intro truck
|
||||
"MAP_INSIDE_OF_TRUCK:0,1,2/MAP_DYNAMIC:-1!",
|
||||
|
||||
# Battle Frontier
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0",
|
||||
"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11",
|
||||
"MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2",
|
||||
"MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4",
|
||||
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8",
|
||||
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9",
|
||||
"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5",
|
||||
|
||||
"MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13",
|
||||
"MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1",
|
||||
"MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10",
|
||||
"MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1",
|
||||
|
||||
# Terra Cave and Marine Cave
|
||||
"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!",
|
||||
"MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1",
|
||||
"MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0",
|
||||
"MAP_ROUTE113:1/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE113:2/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE114:3/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE114:4/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE115:1/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE115:2/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE116:3/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE116:4/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE118:0/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
"MAP_ROUTE118:1/MAP_TERRA_CAVE_ENTRANCE:0!",
|
||||
|
||||
"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!",
|
||||
"MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0",
|
||||
"MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0",
|
||||
"MAP_UNDERWATER_ROUTE105:0/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE105:1/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE125:0/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE125:1/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE127:0/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE127:1/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE129:0/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
"MAP_UNDERWATER_ROUTE129:1/MAP_UNDERWATER_MARINE_CAVE:0!",
|
||||
|
||||
# Event islands
|
||||
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0",
|
||||
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0",
|
||||
|
||||
"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1",
|
||||
"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1",
|
||||
|
||||
"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1",
|
||||
"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1",
|
||||
|
||||
"MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
|
||||
"MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1",
|
||||
"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0",
|
||||
"MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
|
||||
"MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0",
|
||||
"MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1",
|
||||
"MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
|
||||
"MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
|
||||
"MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0",
|
||||
"MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1",
|
||||
"MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
|
||||
"MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
|
||||
"MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0",
|
||||
"MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1",
|
||||
"MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
|
||||
"MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
|
||||
"MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0",
|
||||
"MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1",
|
||||
"MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
|
||||
"MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
|
||||
"MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0",
|
||||
"MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1",
|
||||
"MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
|
||||
"MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0",
|
||||
"MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
|
||||
"MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
|
||||
"MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1",
|
||||
"MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
|
||||
"MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1",
|
||||
"MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
|
||||
"MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
|
||||
"MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0",
|
||||
"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0",
|
||||
"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1",
|
||||
"MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0",
|
||||
"MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
|
||||
"MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
|
||||
"MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0",
|
||||
"MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1",
|
||||
"MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
|
||||
"MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
|
||||
"MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0",
|
||||
|
||||
# Secret bases
|
||||
"MAP_SECRET_BASE_BROWN_CAVE1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BROWN_CAVE2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BROWN_CAVE3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BROWN_CAVE4:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BLUE_CAVE1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BLUE_CAVE2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BLUE_CAVE3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_BLUE_CAVE4:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_YELLOW_CAVE1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_YELLOW_CAVE2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_YELLOW_CAVE3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_YELLOW_CAVE4:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_RED_CAVE1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_RED_CAVE2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_RED_CAVE3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_RED_CAVE4:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_SHRUB1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_SHRUB2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_SHRUB3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_SHRUB4:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_TREE1:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_TREE2:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_TREE3:0/MAP_DYNAMIC:-2!",
|
||||
"MAP_SECRET_BASE_TREE4:0/MAP_DYNAMIC:-2!",
|
||||
|
||||
# Multiplayer rooms
|
||||
"MAP_RECORD_CORNER:0,1,2,3/MAP_DYNAMIC:-1!",
|
||||
|
||||
"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!",
|
||||
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_PETALBURG_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_OLDALE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_FORTREE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
|
||||
|
||||
"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!",
|
||||
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_PETALBURG_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_OLDALE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_FORTREE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
|
||||
|
||||
"MAP_BATTLE_COLOSSEUM_2P:0,1/MAP_DYNAMIC:-1!",
|
||||
"MAP_BATTLE_COLOSSEUM_4P:0,1,2,3/MAP_DYNAMIC:-1!",
|
||||
|
||||
# Unused content
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:0/MAP_CAVE_OF_ORIGIN_1F:1!",
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0",
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1",
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0",
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1",
|
||||
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:1/MAP_CAVE_OF_ORIGIN_B1F:0!",
|
||||
"MAP_LILYCOVE_CITY_UNUSED_MART:0,1/MAP_LILYCOVE_CITY:0!"
|
||||
}
|
||||
|
||||
|
||||
def validate_regions() -> bool:
|
||||
error_messages: List[str] = []
|
||||
warn_messages: List[str] = []
|
||||
failed = False
|
||||
|
||||
def error(message: str) -> None:
|
||||
nonlocal failed
|
||||
failed = True
|
||||
error_messages.append(message)
|
||||
|
||||
def warn(message: str) -> None:
|
||||
warn_messages.append(message)
|
||||
|
||||
# Check regions
|
||||
for name, region in data.regions.items():
|
||||
for region_exit in region.exits:
|
||||
if region_exit not in data.regions:
|
||||
error(f"Pokemon Emerald: Region [{region_exit}] referenced by [{name}] was not defined")
|
||||
|
||||
# Check warps
|
||||
for warp_source, warp_dest in data.warp_map.items():
|
||||
if warp_source in _ignorable_warps:
|
||||
continue
|
||||
|
||||
if warp_dest is None:
|
||||
error(f"Pokemon Emerald: Warp [{warp_source}] has no destination")
|
||||
elif not data.warps[warp_dest].connects_to(data.warps[warp_source]) and not data.warps[warp_source].is_one_way:
|
||||
error(f"Pokemon Emerald: Warp [{warp_source}] appears to be a one-way warp but was not marked as one")
|
||||
|
||||
# Check locations
|
||||
claimed_locations = [location for region in data.regions.values() for location in region.locations]
|
||||
claimed_locations_set = set()
|
||||
for location_name in claimed_locations:
|
||||
if location_name in claimed_locations_set:
|
||||
error(f"Pokemon Emerald: Location [{location_name}] was claimed by multiple regions")
|
||||
claimed_locations_set.add(location_name)
|
||||
|
||||
for location_name in data.locations:
|
||||
if location_name not in claimed_locations and location_name not in _ignorable_locations:
|
||||
warn(f"Pokemon Emerald: Location [{location_name}] was not claimed by any region")
|
||||
|
||||
warn_messages.sort()
|
||||
error_messages.sort()
|
||||
|
||||
for message in warn_messages:
|
||||
logging.warning(message)
|
||||
for message in error_messages:
|
||||
logging.error(message)
|
||||
|
||||
logging.debug("Pokemon Emerald sanity check done. Found %s errors and %s warnings.", len(error_messages), len(warn_messages))
|
||||
|
||||
return not failed
|
|
@ -0,0 +1,5 @@
|
|||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class PokemonEmeraldTestBase(WorldTestBase):
|
||||
game = "Pokemon Emerald"
|
|
@ -0,0 +1,178 @@
|
|||
from Options import Toggle
|
||||
|
||||
from . import PokemonEmeraldTestBase
|
||||
from ..util import location_name_to_label
|
||||
from ..options import NormanRequirement
|
||||
|
||||
|
||||
class TestBasic(PokemonEmeraldTestBase):
|
||||
def test_always_accessible(self) -> None:
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_102_POTION")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_SUPER_POTION")))
|
||||
|
||||
|
||||
class TestSurf(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"npc_gifts": Toggle.option_true
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_surf(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
|
||||
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
|
||||
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
|
||||
|
||||
def test_accessible_with_surf_only(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
|
||||
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
|
||||
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_4")))
|
||||
|
||||
|
||||
class TestFreeFly(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"npc_gifts": Toggle.option_true,
|
||||
"free_fly_location": Toggle.option_true
|
||||
}
|
||||
|
||||
def setUp(self) -> None:
|
||||
super(PokemonEmeraldTestBase, self).setUp()
|
||||
|
||||
# Swap free fly to Sootopolis
|
||||
free_fly_location = self.multiworld.get_location("FREE_FLY_LOCATION", 1)
|
||||
free_fly_location.item = None
|
||||
free_fly_location.place_locked_item(self.multiworld.worlds[1].create_event("EVENT_VISITED_SOOTOPOLIS_CITY"))
|
||||
|
||||
def test_sootopolis_gift_inaccessible_with_no_surf(self) -> None:
|
||||
self.collect_by_name(["HM02 Fly", "Feather Badge"])
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM31")))
|
||||
|
||||
def test_sootopolis_gift_accessible_with_surf(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge", "HM02 Fly", "Feather Badge"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM31")))
|
||||
|
||||
|
||||
class TestFerry(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"npc_gifts": Toggle.option_true,
|
||||
"enable_ferry": Toggle.option_true
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_items(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
|
||||
def test_inaccessible_with_only_slateport_access(self) -> None:
|
||||
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
|
||||
def test_accessible_with_slateport_access_and_ticket(self) -> None:
|
||||
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike", "S.S. Ticket"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
|
||||
|
||||
|
||||
class TestExtraBouldersOn(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"extra_boulders": Toggle.option_true
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_items(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
|
||||
|
||||
def test_inaccessible_with_surf_only(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge"])
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
|
||||
|
||||
def test_accessible_with_surf_and_strength(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge", "HM04 Strength", "Heat Badge"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
|
||||
|
||||
|
||||
class TestExtraBouldersOff(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"extra_boulders": Toggle.option_false
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_items(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
|
||||
|
||||
def test_accessible_with_surf_only(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
|
||||
|
||||
|
||||
class TestNormanRequirement1(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"norman_requirement": NormanRequirement.option_badges,
|
||||
"norman_count": 0
|
||||
}
|
||||
|
||||
def test_accessible_with_no_items(self) -> None:
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
|
||||
class TestNormanRequirement2(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"norman_requirement": NormanRequirement.option_badges,
|
||||
"norman_count": 4
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_items(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
def test_accessible_with_enough_badges(self) -> None:
|
||||
self.collect_by_name(["Stone Badge", "Knuckle Badge", "Feather Badge", "Balance Badge"])
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
|
||||
class TestNormanRequirement3(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"norman_requirement": NormanRequirement.option_gyms,
|
||||
"norman_count": 0
|
||||
}
|
||||
|
||||
def test_accessible_with_no_items(self) -> None:
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
|
||||
class TestNormanRequirement4(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"norman_requirement": NormanRequirement.option_gyms,
|
||||
"norman_count": 4
|
||||
}
|
||||
|
||||
def test_inaccessible_with_no_items(self) -> None:
|
||||
self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
def test_accessible_with_reachable_gyms(self) -> None:
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge"]) # Reaches Roxanne, Brawley, Wattson, and Flannery
|
||||
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
|
||||
|
||||
|
||||
class TestVictoryRoad(PokemonEmeraldTestBase):
|
||||
options = {
|
||||
"elite_four_requirement": NormanRequirement.option_badges,
|
||||
"elite_four_count": 0,
|
||||
"remove_roadblocks": {"Lilycove City Wailmer"}
|
||||
}
|
||||
|
||||
def test_accessible_with_specific_hms(self) -> None:
|
||||
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
||||
self.collect_by_name(["HM03 Surf", "Balance Badge"])
|
||||
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
||||
self.collect_by_name(["HM07 Waterfall", "Rain Badge"])
|
||||
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
||||
self.collect_by_name(["HM04 Strength", "Heat Badge"])
|
||||
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
||||
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge"])
|
||||
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
||||
self.collect_by_name(["HM05 Flash", "Knuckle Badge"])
|
||||
self.assertTrue(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
|
|
@ -0,0 +1,21 @@
|
|||
from test.TestBase import TestBase
|
||||
from ..data import Warp
|
||||
|
||||
|
||||
class TestWarps(TestBase):
|
||||
def test_warps_connect_ltr(self) -> None:
|
||||
# 2-way
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0")))
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:0")))
|
||||
|
||||
# 1-way
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
|
||||
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:3")))
|
||||
|
||||
# Invalid
|
||||
self.assertFalse(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
|
||||
self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
|
||||
self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_C:2/FAKE_MAP_A:0")))
|
|
@ -0,0 +1,19 @@
|
|||
from typing import List
|
||||
|
||||
from .data import data
|
||||
|
||||
|
||||
def location_name_to_label(name: str) -> str:
|
||||
return data.locations[name].label
|
||||
|
||||
|
||||
def int_to_bool_array(num: int) -> List[bool]:
|
||||
binary_string = format(num, '064b')
|
||||
bool_array = [bit == '1' for bit in reversed(binary_string)]
|
||||
return bool_array
|
||||
|
||||
|
||||
def bool_array_to_int(bool_array: List[bool]) -> int:
|
||||
binary_string = ''.join(['1' if bit else '0' for bit in reversed(bool_array)])
|
||||
num = int(binary_string, 2)
|
||||
return num
|
Loading…
Reference in New Issue