Pokemon Emerald: Randomize rock smash encounters (#3912)

* Pokemon Emerald: WIP add rock smash encounter randomization

* Pokemon Emerald: Refactor encounter data on maps

* Pokemon Emerald: Remove unused import

* Pokemon Emerald: Swap StrEnum for regular Enum and use .value
This commit is contained in:
Bryce Wilson
2025-03-08 08:57:16 -08:00
committed by GitHub
parent 5662da6f7d
commit 3986f6f11a
7 changed files with 179 additions and 181 deletions

View File

@@ -27,6 +27,7 @@ from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilit
randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters, randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters) randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
from .rom import PokemonEmeraldProcedurePatch, write_tokens from .rom import PokemonEmeraldProcedurePatch, write_tokens
from .util import get_encounter_type_label
class PokemonEmeraldWebWorld(WebWorld): class PokemonEmeraldWebWorld(WebWorld):
@@ -636,32 +637,11 @@ class PokemonEmeraldWorld(World):
spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")
slot_to_rod_suffix = {
0: " (Old Rod)",
1: " (Old Rod)",
2: " (Good Rod)",
3: " (Good Rod)",
4: " (Good Rod)",
5: " (Super Rod)",
6: " (Super Rod)",
7: " (Super Rod)",
8: " (Super Rod)",
9: " (Super Rod)",
}
species_maps = defaultdict(set) species_maps = defaultdict(set)
for map in self.modified_maps.values(): for map_data in self.modified_maps.values():
if map.land_encounters is not None: for encounter_type, encounter_data in map_data.encounters.items():
for encounter in map.land_encounters.slots: for i, encounter in enumerate(encounter_data.slots):
species_maps[encounter].add(map.label + " (Land)") species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
if map.water_encounters is not None:
for encounter in map.water_encounters.slots:
species_maps[encounter].add(map.label + " (Water)")
if map.fishing_encounters is not None:
for slot, encounter in enumerate(map.fishing_encounters.slots):
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n" lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n"
for species, maps in species_maps.items()] for species, maps in species_maps.items()]
@@ -675,32 +655,11 @@ class PokemonEmeraldWorld(World):
if self.options.dexsanity: if self.options.dexsanity:
from collections import defaultdict from collections import defaultdict
slot_to_rod_suffix = {
0: " (Old Rod)",
1: " (Old Rod)",
2: " (Good Rod)",
3: " (Good Rod)",
4: " (Good Rod)",
5: " (Super Rod)",
6: " (Super Rod)",
7: " (Super Rod)",
8: " (Super Rod)",
9: " (Super Rod)",
}
species_maps = defaultdict(set) species_maps = defaultdict(set)
for map in self.modified_maps.values(): for map_data in self.modified_maps.values():
if map.land_encounters is not None: for encounter_type, encounter_data in map_data.encounters.items():
for encounter in map.land_encounters.slots: for i, encounter in enumerate(encounter_data.slots):
species_maps[encounter].add(map.label + " (Land)") species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
if map.water_encounters is not None:
for encounter in map.water_encounters.slots:
species_maps[encounter].add(map.label + " (Water)")
if map.fishing_encounters is not None:
for slot, encounter in enumerate(map.fishing_encounters.slots):
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
hint_data[self.player] = { hint_data[self.player] = {
self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps)) self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps))

View File

@@ -5,7 +5,7 @@ defined data (like location labels or usable pokemon species), some cleanup
and sorting, and Warp methods. and sorting, and Warp methods.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum, Enum
import orjson import orjson
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
import pkgutil import pkgutil
@@ -148,14 +148,20 @@ class EncounterTableData(NamedTuple):
address: int address: int
# class EncounterType(StrEnum): # StrEnum introduced in python 3.11
class EncounterType(Enum):
LAND = "LAND"
WATER = "WATER"
FISHING = "FISHING"
ROCK_SMASH = "ROCK_SMASH"
@dataclass @dataclass
class MapData: class MapData:
name: str name: str
label: str label: str
header_address: int header_address: int
land_encounters: Optional[EncounterTableData] encounters: Dict[EncounterType, EncounterTableData]
water_encounters: Optional[EncounterTableData]
fishing_encounters: Optional[EncounterTableData]
class EventData(NamedTuple): class EventData(NamedTuple):
@@ -348,25 +354,27 @@ def _init() -> None:
if map_name in IGNORABLE_MAPS: if map_name in IGNORABLE_MAPS:
continue continue
land_encounters = None encounter_tables: Dict[EncounterType, EncounterTableData] = {}
water_encounters = None
fishing_encounters = None
if "land_encounters" in map_json: if "land_encounters" in map_json:
land_encounters = EncounterTableData( encounter_tables[EncounterType.LAND] = EncounterTableData(
map_json["land_encounters"]["slots"], map_json["land_encounters"]["slots"],
map_json["land_encounters"]["address"] map_json["land_encounters"]["address"]
) )
if "water_encounters" in map_json: if "water_encounters" in map_json:
water_encounters = EncounterTableData( encounter_tables[EncounterType.WATER] = EncounterTableData(
map_json["water_encounters"]["slots"], map_json["water_encounters"]["slots"],
map_json["water_encounters"]["address"] map_json["water_encounters"]["address"]
) )
if "fishing_encounters" in map_json: if "fishing_encounters" in map_json:
fishing_encounters = EncounterTableData( encounter_tables[EncounterType.FISHING] = EncounterTableData(
map_json["fishing_encounters"]["slots"], map_json["fishing_encounters"]["slots"],
map_json["fishing_encounters"]["address"] map_json["fishing_encounters"]["address"]
) )
if "rock_smash_encounters" in map_json:
encounter_tables[EncounterType.ROCK_SMASH] = EncounterTableData(
map_json["rock_smash_encounters"]["slots"],
map_json["rock_smash_encounters"]["address"]
)
# Derive a user-facing label # Derive a user-facing label
label = [] label = []
@@ -398,9 +406,7 @@ def _init() -> None:
map_name, map_name,
" ".join(label), " ".join(label),
map_json["header_address"], map_json["header_address"],
land_encounters, encounter_tables
water_encounters,
fishing_encounters
) )
# Load/merge region json files # Load/merge region json files

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,8 @@ Functions related to pokemon species and moves
import functools import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data) from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterType, EncounterTableData, LearnsetMove, SpeciesData,
MapData, data)
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility) TmTutorCompatibility)
@@ -226,6 +227,42 @@ def randomize_types(world: "PokemonEmeraldWorld") -> None:
evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions] evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
_encounter_subcategory_ranges: Dict[EncounterType, Dict[range, Optional[str]]] = {
EncounterType.LAND: {range(0, 12): None},
EncounterType.WATER: {range(0, 5): None},
EncounterType.FISHING: {range(0, 2): "OLD_ROD", range(2, 5): "GOOD_ROD", range(5, 10): "SUPER_ROD"},
}
def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slots: List[int], encounter_type: EncounterType):
"""
Renames the events that correspond to wild encounters to reflect the new species there after randomization
"""
for i, new_species_id in enumerate(new_slots):
# Get the subcategory for rods
subcategory_range, subcategory_name = next(
(r, sc)
for r, sc in _encounter_subcategory_ranges[encounter_type].items()
if i in r
)
subcategory_species = []
for k in subcategory_range:
if new_slots[k] not in subcategory_species:
subcategory_species.append(new_slots[k])
# Create the name of the location that corresponds to this encounter slot
# Fishing locations include the rod name
subcategory_str = "" if subcategory_name is None else "_" + subcategory_name
encounter_location_index = subcategory_species.index(new_species_id) + 1
encounter_location_name = f"{map_data.name}_{encounter_type.value}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
try:
# Get the corresponding location and change the event name to reflect the new species
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
except KeyError:
pass # Map probably isn't included; should be careful here about bad encounter location names
def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
return return
@@ -253,120 +290,96 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
placed_priority_species = False placed_priority_species = False
map_data = world.modified_maps[map_name] map_data = world.modified_maps[map_name]
new_encounters: List[Optional[EncounterTableData]] = [None, None, None] new_encounters: Dict[EncounterType, EncounterTableData] = {}
old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
for i, table in enumerate(old_encounters): for encounter_type, table in map_data.encounters.items():
if table is not None: # Create a map from the original species to new species
# Create a map from the original species to new species # instead of just randomizing every slot.
# instead of just randomizing every slot. # Force area 1-to-1 mapping, in other words.
# Force area 1-to-1 mapping, in other words. species_old_to_new_map: Dict[int, int] = {}
species_old_to_new_map: Dict[int, int] = {} for species_id in table.slots:
for species_id in table.slots: if species_id not in species_old_to_new_map:
if species_id not in species_old_to_new_map: if not placed_priority_species and len(priority_species) > 0 \
if not placed_priority_species and len(priority_species) > 0 \ and encounter_type != EncounterType.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS:
and map_name not in OUT_OF_LOGIC_MAPS: new_species_id = priority_species.pop()
new_species_id = priority_species.pop() placed_priority_species = True
placed_priority_species = True else:
else: original_species = data.species[species_id]
original_species = data.species[species_id]
# Construct progressive tiers of blacklists that can be peeled back if they # Construct progressive tiers of blacklists that can be peeled back if they
# collectively cover too much of the pokedex. A lower index in `blacklists` # collectively cover too much of the pokedex. A lower index in `blacklists`
# indicates a more important set of species to avoid. Entries at `0` will # indicates a more important set of species to avoid. Entries at `0` will
# always be blacklisted. # always be blacklisted.
blacklists: Dict[int, List[Set[int]]] = defaultdict(list) blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
# Blacklist pokemon already on this table # Blacklist pokemon already on this table
blacklists[0].append(set(species_old_to_new_map.values())) blacklists[0].append(set(species_old_to_new_map.values()))
# If doing legendary hunt, blacklist Latios from wild encounters so # If doing legendary hunt, blacklist Latios from wild encounters so
# it can be tracked as the roamer. Otherwise it may be impossible # it can be tracked as the roamer. Otherwise it may be impossible
# to tell whether a highlighted route is the roamer or a wild # to tell whether a highlighted route is the roamer or a wild
# encounter. # encounter.
if world.options.goal == Goal.option_legendary_hunt: if world.options.goal == Goal.option_legendary_hunt:
blacklists[0].append({data.constants["SPECIES_LATIOS"]}) blacklists[0].append({data.constants["SPECIES_LATIOS"]})
# If dexsanity/catch 'em all mode, blacklist already placed species # If dexsanity/catch 'em all mode, blacklist already placed species
# until every species has been placed once # until every species has been placed once
if world.options.dexsanity and len(already_placed) < num_placeable_species: if world.options.dexsanity and len(already_placed) < num_placeable_species:
blacklists[1].append(already_placed) blacklists[1].append(already_placed)
# Blacklist from player options # Blacklist from player options
blacklists[2].append(world.blacklisted_wilds) blacklists[2].append(world.blacklisted_wilds)
# Type matching blacklist # Type matching blacklist
if should_match_type: if should_match_type:
blacklists[3].append({ blacklists[3].append({
species.species_id species.species_id
for species in world.modified_species.values()
if not bool(set(species.types) & set(original_species.types))
})
merged_blacklist: Set[int] = set()
for max_priority in reversed(sorted(blacklists.keys())):
merged_blacklist = set()
for priority in blacklists.keys():
if priority <= max_priority:
for blacklist in blacklists[priority]:
merged_blacklist |= blacklist
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
raise RuntimeError("This should never happen")
candidates = [
species
for species in world.modified_species.values() for species in world.modified_species.values()
if species.species_id not in merged_blacklist if not bool(set(species.types) & set(original_species.types))
] })
if should_match_bst: merged_blacklist: Set[int] = set()
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) for max_priority in reversed(sorted(blacklists.keys())):
merged_blacklist = set()
for priority in blacklists.keys():
if priority <= max_priority:
for blacklist in blacklists[priority]:
merged_blacklist |= blacklist
new_species_id = world.random.choice(candidates).species_id if len(merged_blacklist) < NUM_REAL_SPECIES:
species_old_to_new_map[species_id] = new_species_id break
else:
raise RuntimeError("This should never happen")
if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS: candidates = [
already_placed.add(new_species_id) species
for species in world.modified_species.values()
if species.species_id not in merged_blacklist
]
# Actually create the new list of slots and encounter table if should_match_bst:
new_slots: List[int] = [] candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
new_encounters[i] = EncounterTableData(new_slots, table.address) new_species_id = world.random.choice(candidates).species_id
# Rename event items for the new wild pokemon species species_old_to_new_map[species_id] = new_species_id
slot_category: Tuple[str, List[Tuple[Optional[str], range]]] = [
("LAND", [(None, range(0, 12))]),
("WATER", [(None, range(0, 5))]),
("FISHING", [("OLD_ROD", range(0, 2)), ("GOOD_ROD", range(2, 5)), ("SUPER_ROD", range(5, 10))]),
][i]
for j, new_species_id in enumerate(new_slots):
# Get the subcategory for rods
subcategory = next(sc for sc in slot_category[1] if j in sc[1])
subcategory_species = []
for k in subcategory[1]:
if new_slots[k] not in subcategory_species:
subcategory_species.append(new_slots[k])
# Create the name of the location that corresponds to this encounter slot if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \
# Fishing locations include the rod name and map_name not in OUT_OF_LOGIC_MAPS:
subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0] already_placed.add(new_species_id)
encounter_location_index = subcategory_species.index(new_species_id) + 1
encounter_location_name = f"{map_data.name}_{slot_category[0]}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
try:
# Get the corresponding location and change the event name to reflect the new species
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
except KeyError:
pass # Map probably isn't included; should be careful here about bad encounter location names
map_data.land_encounters = new_encounters[0] # Actually create the new list of slots and encounter table
map_data.water_encounters = new_encounters[1] new_slots: List[int] = []
map_data.fishing_encounters = new_encounters[2] for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
new_encounters[encounter_type] = EncounterTableData(new_slots, table.address)
# Rock smash encounters not used in logic, so they have no events
if encounter_type != EncounterType.ROCK_SMASH:
_rename_wild_events(world, map_data, new_slots, encounter_type)
map_data.encounters = new_encounters
def randomize_abilities(world: "PokemonEmeraldWorld") -> None: def randomize_abilities(world: "PokemonEmeraldWorld") -> None:

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from BaseClasses import CollectionState, ItemClassification, Region from BaseClasses import CollectionState, ItemClassification, Region
from .data import data from .data import EncounterType, data
from .items import PokemonEmeraldItem from .items import PokemonEmeraldItem
from .locations import PokemonEmeraldLocation from .locations import PokemonEmeraldLocation
@@ -19,11 +19,11 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
Also creates and places events and connects regions via warps and the exits defined in the JSON. Also creates and places events and connects regions via warps and the exits defined in the JSON.
""" """
# Used in connect_to_map_encounters. Splits encounter categories into "subcategories" and gives them names # Used in connect_to_map_encounters. Splits encounter categories into "subcategories" and gives them names
# and rules so the rods can only access their specific slots. # and rules so the rods can only access their specific slots. Rock smash encounters are not considered in logic.
encounter_categories: Dict[str, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = { encounter_categories: Dict[EncounterType, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
"LAND": [(None, range(0, 12), None)], EncounterType.LAND: [(None, range(0, 12), None)],
"WATER": [(None, range(0, 5), None)], EncounterType.WATER: [(None, range(0, 5), None)],
"FISHING": [ EncounterType.FISHING: [
("OLD_ROD", range(0, 2), lambda state: state.has("Old Rod", world.player)), ("OLD_ROD", range(0, 2), lambda state: state.has("Old Rod", world.player)),
("GOOD_ROD", range(2, 5), lambda state: state.has("Good Rod", world.player)), ("GOOD_ROD", range(2, 5), lambda state: state.has("Good Rod", world.player)),
("SUPER_ROD", range(5, 10), lambda state: state.has("Super Rod", world.player)), ("SUPER_ROD", range(5, 10), lambda state: state.has("Super Rod", world.player)),
@@ -41,19 +41,19 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
These regions are created lazily and dynamically so as not to bother with unused maps. These regions are created lazily and dynamically so as not to bother with unused maps.
""" """
# For each of land, water, and fishing, connect the region if indicated by include_slots # For each of land, water, and fishing, connect the region if indicated by include_slots
for i, encounter_category in enumerate(encounter_categories.items()): for i, (encounter_type, subcategories) in enumerate(encounter_categories.items()):
if include_slots[i]: if include_slots[i]:
region_name = f"{map_name}_{encounter_category[0]}_ENCOUNTERS" region_name = f"{map_name}_{encounter_type.value}_ENCOUNTERS"
# If the region hasn't been created yet, create it now # If the region hasn't been created yet, create it now
try: try:
encounter_region = world.multiworld.get_region(region_name, world.player) encounter_region = world.multiworld.get_region(region_name, world.player)
except KeyError: except KeyError:
encounter_region = Region(region_name, world.player, world.multiworld) encounter_region = Region(region_name, world.player, world.multiworld)
encounter_slots = getattr(data.maps[map_name], f"{encounter_category[0].lower()}_encounters").slots encounter_slots = data.maps[map_name].encounters[encounter_type].slots
# Subcategory is for splitting fishing rods; land and water only have one subcategory # Subcategory is for splitting fishing rods; land and water only have one subcategory
for subcategory in encounter_category[1]: for subcategory in subcategories:
# Want to create locations per species, not per slot # Want to create locations per species, not per slot
# encounter_categories includes info on which slots belong to which subcategory # encounter_categories includes info on which slots belong to which subcategory
unique_species = [] unique_species = []

View File

@@ -696,12 +696,10 @@ def _set_encounter_tables(world: "PokemonEmeraldWorld", patch: PokemonEmeraldPro
} }
""" """
for map_data in world.modified_maps.values(): for map_data in world.modified_maps.values():
tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters] for table in map_data.encounters.values():
for table in tables: for i, species_id in enumerate(table.slots):
if table is not None: address = table.address + 2 + (4 * i)
for i, species_id in enumerate(table.slots): patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
address = table.address + 2 + (4 * i)
patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
def _set_species_info(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None: def _set_species_info(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:

View File

@@ -1,7 +1,7 @@
import orjson import orjson
from typing import Any, Dict, List, Optional, Tuple, Iterable from typing import Any, Dict, List, Optional, Tuple, Iterable
from .data import NATIONAL_ID_TO_SPECIES_ID, data from .data import NATIONAL_ID_TO_SPECIES_ID, EncounterType, data
CHARACTER_DECODING_MAP = { CHARACTER_DECODING_MAP = {
@@ -86,6 +86,28 @@ def decode_string(string_data: Iterable[int]) -> str:
return string return string
def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str:
if encounter_type == EncounterType.FISHING:
return {
0: "Old Rod",
1: "Old Rod",
2: "Good Rod",
3: "Good Rod",
4: "Good Rod",
5: "Super Rod",
6: "Super Rod",
7: "Super Rod",
8: "Super Rod",
9: "Super Rod",
}[slot]
return {
EncounterType.LAND: 'Land',
EncounterType.WATER: 'Water',
EncounterType.ROCK_SMASH: 'Rock Smash',
}[encounter_type]
def get_easter_egg(easter_egg: str) -> Tuple[int, int]: def get_easter_egg(easter_egg: str) -> Tuple[int, int]:
easter_egg = easter_egg.upper() easter_egg = easter_egg.upper()
result1 = 0 result1 = 0