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_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
from .rom import PokemonEmeraldProcedurePatch, write_tokens
from .util import get_encounter_type_label
class PokemonEmeraldWebWorld(WebWorld):
@@ -636,32 +637,11 @@ class PokemonEmeraldWorld(World):
spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")
slot_to_rod_suffix = {
0: " (Old Rod)",
1: " (Old Rod)",
2: " (Good Rod)",
3: " (Good Rod)",
4: " (Good Rod)",
5: " (Super Rod)",
6: " (Super Rod)",
7: " (Super Rod)",
8: " (Super Rod)",
9: " (Super Rod)",
}
species_maps = defaultdict(set)
for map in self.modified_maps.values():
if map.land_encounters is not None:
for encounter in map.land_encounters.slots:
species_maps[encounter].add(map.label + " (Land)")
if map.water_encounters is not None:
for encounter in map.water_encounters.slots:
species_maps[encounter].add(map.label + " (Water)")
if map.fishing_encounters is not None:
for slot, encounter in enumerate(map.fishing_encounters.slots):
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
for map_data in self.modified_maps.values():
for encounter_type, encounter_data in map_data.encounters.items():
for i, encounter in enumerate(encounter_data.slots):
species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n"
for species, maps in species_maps.items()]
@@ -675,32 +655,11 @@ class PokemonEmeraldWorld(World):
if self.options.dexsanity:
from collections import defaultdict
slot_to_rod_suffix = {
0: " (Old Rod)",
1: " (Old Rod)",
2: " (Good Rod)",
3: " (Good Rod)",
4: " (Good Rod)",
5: " (Super Rod)",
6: " (Super Rod)",
7: " (Super Rod)",
8: " (Super Rod)",
9: " (Super Rod)",
}
species_maps = defaultdict(set)
for map in self.modified_maps.values():
if map.land_encounters is not None:
for encounter in map.land_encounters.slots:
species_maps[encounter].add(map.label + " (Land)")
if map.water_encounters is not None:
for encounter in map.water_encounters.slots:
species_maps[encounter].add(map.label + " (Water)")
if map.fishing_encounters is not None:
for slot, encounter in enumerate(map.fishing_encounters.slots):
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
for map_data in self.modified_maps.values():
for encounter_type, encounter_data in map_data.encounters.items():
for i, encounter in enumerate(encounter_data.slots):
species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
hint_data[self.player] = {
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.
"""
from dataclasses import dataclass
from enum import IntEnum
from enum import IntEnum, Enum
import orjson
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
import pkgutil
@@ -148,14 +148,20 @@ class EncounterTableData(NamedTuple):
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
class MapData:
name: str
label: str
header_address: int
land_encounters: Optional[EncounterTableData]
water_encounters: Optional[EncounterTableData]
fishing_encounters: Optional[EncounterTableData]
encounters: Dict[EncounterType, EncounterTableData]
class EventData(NamedTuple):
@@ -348,25 +354,27 @@ def _init() -> None:
if map_name in IGNORABLE_MAPS:
continue
land_encounters = None
water_encounters = None
fishing_encounters = None
encounter_tables: Dict[EncounterType, EncounterTableData] = {}
if "land_encounters" in map_json:
land_encounters = EncounterTableData(
encounter_tables[EncounterType.LAND] = EncounterTableData(
map_json["land_encounters"]["slots"],
map_json["land_encounters"]["address"]
)
if "water_encounters" in map_json:
water_encounters = EncounterTableData(
encounter_tables[EncounterType.WATER] = EncounterTableData(
map_json["water_encounters"]["slots"],
map_json["water_encounters"]["address"]
)
if "fishing_encounters" in map_json:
fishing_encounters = EncounterTableData(
encounter_tables[EncounterType.FISHING] = EncounterTableData(
map_json["fishing_encounters"]["slots"],
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
label = []
@@ -398,9 +406,7 @@ def _init() -> None:
map_name,
" ".join(label),
map_json["header_address"],
land_encounters,
water_encounters,
fishing_encounters
encounter_tables
)
# 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
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,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
@@ -226,6 +227,42 @@ def randomize_types(world: "PokemonEmeraldWorld") -> None:
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:
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
return
@@ -253,120 +290,96 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
placed_priority_species = False
map_data = world.modified_maps[map_name]
new_encounters: List[Optional[EncounterTableData]] = [None, None, None]
old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
new_encounters: Dict[EncounterType, EncounterTableData] = {}
for i, table in enumerate(old_encounters):
if table is not None:
# Create a map from the original species to new species
# instead of just randomizing every slot.
# Force area 1-to-1 mapping, in other words.
species_old_to_new_map: Dict[int, int] = {}
for species_id in table.slots:
if species_id not in species_old_to_new_map:
if not placed_priority_species and len(priority_species) > 0 \
and map_name not in OUT_OF_LOGIC_MAPS:
new_species_id = priority_species.pop()
placed_priority_species = True
else:
original_species = data.species[species_id]
for encounter_type, table in map_data.encounters.items():
# Create a map from the original species to new species
# instead of just randomizing every slot.
# Force area 1-to-1 mapping, in other words.
species_old_to_new_map: Dict[int, int] = {}
for species_id in table.slots:
if species_id not in species_old_to_new_map:
if not placed_priority_species and len(priority_species) > 0 \
and encounter_type != EncounterType.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS:
new_species_id = priority_species.pop()
placed_priority_species = True
else:
original_species = data.species[species_id]
# Construct progressive tiers of blacklists that can be peeled back if they
# collectively cover too much of the pokedex. A lower index in `blacklists`
# indicates a more important set of species to avoid. Entries at `0` will
# always be blacklisted.
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
# Construct progressive tiers of blacklists that can be peeled back if they
# collectively cover too much of the pokedex. A lower index in `blacklists`
# indicates a more important set of species to avoid. Entries at `0` will
# always be blacklisted.
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
# Blacklist pokemon already on this table
blacklists[0].append(set(species_old_to_new_map.values()))
# Blacklist pokemon already on this table
blacklists[0].append(set(species_old_to_new_map.values()))
# If doing legendary hunt, blacklist Latios from wild encounters so
# it can be tracked as the roamer. Otherwise it may be impossible
# to tell whether a highlighted route is the roamer or a wild
# encounter.
if world.options.goal == Goal.option_legendary_hunt:
blacklists[0].append({data.constants["SPECIES_LATIOS"]})
# If doing legendary hunt, blacklist Latios from wild encounters so
# it can be tracked as the roamer. Otherwise it may be impossible
# to tell whether a highlighted route is the roamer or a wild
# encounter.
if world.options.goal == Goal.option_legendary_hunt:
blacklists[0].append({data.constants["SPECIES_LATIOS"]})
# If dexsanity/catch 'em all mode, blacklist already placed species
# until every species has been placed once
if world.options.dexsanity and len(already_placed) < num_placeable_species:
blacklists[1].append(already_placed)
# If dexsanity/catch 'em all mode, blacklist already placed species
# until every species has been placed once
if world.options.dexsanity and len(already_placed) < num_placeable_species:
blacklists[1].append(already_placed)
# Blacklist from player options
blacklists[2].append(world.blacklisted_wilds)
# Blacklist from player options
blacklists[2].append(world.blacklisted_wilds)
# Type matching blacklist
if should_match_type:
blacklists[3].append({
species.species_id
for species in world.modified_species.values()
if not bool(set(species.types) & set(original_species.types))
})
merged_blacklist: Set[int] = set()
for max_priority in reversed(sorted(blacklists.keys())):
merged_blacklist = set()
for priority in blacklists.keys():
if priority <= max_priority:
for blacklist in blacklists[priority]:
merged_blacklist |= blacklist
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
raise RuntimeError("This should never happen")
candidates = [
species
# Type matching blacklist
if should_match_type:
blacklists[3].append({
species.species_id
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:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
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
new_species_id = world.random.choice(candidates).species_id
species_old_to_new_map[species_id] = new_species_id
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
raise RuntimeError("This should never happen")
if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS:
already_placed.add(new_species_id)
candidates = [
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
new_slots: List[int] = []
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
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
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])
species_old_to_new_map[species_id] = new_species_id
# Create the name of the location that corresponds to this encounter slot
# Fishing locations include the rod name
subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0]
encounter_location_index = subcategory_species.index(new_species_id) + 1
encounter_location_name = f"{map_data.name}_{slot_category[0]}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
try:
# Get the corresponding location and change the event name to reflect the new species
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
except KeyError:
pass # Map probably isn't included; should be careful here about bad encounter location names
if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \
and map_name not in OUT_OF_LOGIC_MAPS:
already_placed.add(new_species_id)
map_data.land_encounters = new_encounters[0]
map_data.water_encounters = new_encounters[1]
map_data.fishing_encounters = new_encounters[2]
# Actually create the new list of slots and encounter table
new_slots: List[int] = []
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
new_encounters[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:

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from BaseClasses import CollectionState, ItemClassification, Region
from .data import data
from .data import EncounterType, data
from .items import PokemonEmeraldItem
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.
"""
# 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.
encounter_categories: Dict[str, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
"LAND": [(None, range(0, 12), None)],
"WATER": [(None, range(0, 5), None)],
"FISHING": [
# and rules so the rods can only access their specific slots. Rock smash encounters are not considered in logic.
encounter_categories: Dict[EncounterType, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
EncounterType.LAND: [(None, range(0, 12), None)],
EncounterType.WATER: [(None, range(0, 5), None)],
EncounterType.FISHING: [
("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)),
("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.
"""
# 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]:
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
try:
encounter_region = world.multiworld.get_region(region_name, world.player)
except KeyError:
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
for subcategory in encounter_category[1]:
for subcategory in subcategories:
# Want to create locations per species, not per slot
# encounter_categories includes info on which slots belong to which subcategory
unique_species = []

View File

@@ -696,12 +696,10 @@ def _set_encounter_tables(world: "PokemonEmeraldWorld", patch: PokemonEmeraldPro
}
"""
for map_data in world.modified_maps.values():
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.address + 2 + (4 * i)
patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
for table in map_data.encounters.values():
for i, species_id in enumerate(table.slots):
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:

View File

@@ -1,7 +1,7 @@
import orjson
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 = {
@@ -86,6 +86,28 @@ def decode_string(string_data: Iterable[int]) -> str:
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]:
easter_egg = easter_egg.upper()
result1 = 0