diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index d2173beb10f..ce67d11c930 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2.6.0 + +### Features + +- Added `dexsanity_encounter_types` and `world_version` to slot data for trackers. + +# 2.5.1 + +### Fixes + +- Fixed dexsanity logic to correctly account only for catching species in their enabled encounter types, even if they +can be found elsewhere. + # 2.5.0 ### Features diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index fb683df90d0..87137ca79cc 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -8,16 +8,18 @@ import os import pkgutil from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union -from BaseClasses import CollectionState, ItemClassification, MultiWorld, Tutorial, LocationProgressType +from BaseClasses import CollectionState, Item, ItemClassification, MultiWorld, Tutorial, LocationProgressType from Fill import FillError, fill_restrictive from Options import OptionError, Toggle import settings from worlds.AutoWorld import WebWorld, World from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient -from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data +from .data import (LEGENDARY_POKEMON, PokemonSource, MapData, SpeciesData, TrainerData, LocationCategory, + data as emerald_data) from .groups import ITEM_GROUPS, LOCATION_GROUPS -from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value +from .items import (PokemonEmeraldItem, PokemonEmeraldObtainPokemonEventItem, create_item_label_to_code_map, + get_item_classification, offset_item_value) from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category, set_free_fly, set_legendary_cave_entrances) from .opponents import randomize_opponent_parties @@ -124,6 +126,7 @@ class PokemonEmeraldWorld(World): blacklisted_starters: Set[int] blacklisted_opponent_pokemon: Set[int] allowed_dexsanity_species: set[int] + enabled_dexsanity_encounter_types: set[PokemonSource] hm_requirements: Dict[str, Union[int, List[str]]] auth: bytes @@ -144,6 +147,7 @@ class PokemonEmeraldWorld(World): self.blacklisted_starters = set() self.blacklisted_opponent_pokemon = set() self.allowed_dexsanity_species = set() + self.enabled_dexsanity_encounter_types = set() self.modified_maps = copy.deepcopy(emerald_data.maps) self.modified_species = copy.deepcopy(emerald_data.species) self.modified_tmhm_moves = [] @@ -201,6 +205,16 @@ class PokemonEmeraldWorld(World): if "_Legendaries" in self.options.trainer_party_blacklist.value: self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON + encounter_table = { + "Land": PokemonSource.LAND, + "Water": PokemonSource.WATER, + "Fishing": PokemonSource.FISHING, + } + self.enabled_dexsanity_encounter_types = { + encounter_table[encounter_type] + for encounter_type in self.options.dexsanity_encounter_types.value + } + # In race mode we don't patch any item location information into the ROM if self.multiworld.is_race and not self.options.remote_items: logging.warning("Pokemon Emerald: Forcing player %s (%s) to use remote items due to race mode.", @@ -739,9 +753,11 @@ class PokemonEmeraldWorld(World): "modify_118", "death_link", "normalize_encounter_rates", + "dexsanity_encounter_types", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements + slot_data["world_version"] = self.world_version return slot_data def create_item(self, name: str) -> PokemonEmeraldItem: @@ -762,3 +778,31 @@ class PokemonEmeraldWorld(World): None, self.player ) + + def collect(self, state: CollectionState, item: Item) -> bool: + changed = super().collect(state, item) + if changed: + if isinstance(item, PokemonEmeraldObtainPokemonEventItem): + if item.source in self.enabled_dexsanity_encounter_types: + state.prog_items[self.player].update({ + f"DEXSANITY_{emerald_data.species[item.species].name}": 1, + }) + if item.species in (emerald_data.constants["SPECIES_WAILORD"], emerald_data.constants["SPECIES_RELICANTH"]): + state.prog_items[self.player].update({ + f"REGI_WALL_{emerald_data.species[item.species].name}": 1, + }) + return changed + + def remove(self, state: CollectionState, item: Item) -> bool: + changed = super().remove(state, item) + if changed: + if isinstance(item, PokemonEmeraldObtainPokemonEventItem): + if item.source in self.enabled_dexsanity_encounter_types: + state.prog_items[self.player].subtract({ + f"DEXSANITY_{emerald_data.species[item.species].name}": 1, + }) + if item.species in (emerald_data.constants["SPECIES_WAILORD"], emerald_data.constants["SPECIES_RELICANTH"]): + state.prog_items[self.player].subtract({ + f"REGI_WALL_{emerald_data.species[item.species].name}": 1, + }) + return changed diff --git a/worlds/pokemon_emerald/archipelago.json b/worlds/pokemon_emerald/archipelago.json index 753ccb1f33d..d8041878f69 100644 --- a/worlds/pokemon_emerald/archipelago.json +++ b/worlds/pokemon_emerald/archipelago.json @@ -1,6 +1,6 @@ { "game": "Pokemon Emerald", - "world_version": "2.5.0", + "world_version": "2.6.0", "minimum_ap_version": "0.6.1", "authors": ["Zunawe"] } diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index 5b5d65369cc..466e2043692 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -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, Enum +from enum import IntEnum, StrEnum import orjson from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union import pkgutil @@ -148,8 +148,7 @@ class EncounterTableData(NamedTuple): address: int -# class EncounterType(StrEnum): # StrEnum introduced in python 3.11 -class EncounterType(Enum): +class PokemonSource(StrEnum): LAND = "LAND" WATER = "WATER" FISHING = "FISHING" @@ -161,7 +160,7 @@ class MapData: name: str label: str header_address: int - encounters: Dict[EncounterType, EncounterTableData] + encounters: Dict[PokemonSource, EncounterTableData] class EventData(NamedTuple): @@ -354,24 +353,24 @@ def _init() -> None: if map_name in IGNORABLE_MAPS: continue - encounter_tables: Dict[EncounterType, EncounterTableData] = {} + encounter_tables: Dict[PokemonSource, EncounterTableData] = {} if "land_encounters" in map_json: - encounter_tables[EncounterType.LAND] = EncounterTableData( + encounter_tables[PokemonSource.LAND] = EncounterTableData( map_json["land_encounters"]["slots"], map_json["land_encounters"]["address"] ) if "water_encounters" in map_json: - encounter_tables[EncounterType.WATER] = EncounterTableData( + encounter_tables[PokemonSource.WATER] = EncounterTableData( map_json["water_encounters"]["slots"], map_json["water_encounters"]["address"] ) if "fishing_encounters" in map_json: - encounter_tables[EncounterType.FISHING] = EncounterTableData( + encounter_tables[PokemonSource.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( + encounter_tables[PokemonSource.ROCK_SMASH] = EncounterTableData( map_json["rock_smash_encounters"]["slots"], map_json["rock_smash_encounters"]["address"] ) diff --git a/worlds/pokemon_emerald/items.py b/worlds/pokemon_emerald/items.py index 922bbbc0dbf..bb8f8637ba5 100644 --- a/worlds/pokemon_emerald/items.py +++ b/worlds/pokemon_emerald/items.py @@ -1,11 +1,11 @@ """ Classes and functions related to AP items for Pokemon Emerald """ -from typing import Dict, FrozenSet, Set, Optional +from typing import Dict, FrozenSet, Optional from BaseClasses import Item, ItemClassification -from .data import BASE_OFFSET, data +from .data import BASE_OFFSET, PokemonSource, data class PokemonEmeraldItem(Item): @@ -21,6 +21,20 @@ class PokemonEmeraldItem(Item): self.tags = data.items[reverse_offset_item_value(code)].tags +class PokemonEmeraldObtainPokemonEventItem(PokemonEmeraldItem): + source: PokemonSource + species: int + + def __init__(self, player: int, source: PokemonSource, species: int) -> None: + super().__init__("", ItemClassification.progression, None, player) + self.replace_species(species) + self.source = source + + def replace_species(self, new_species: int): + self.species = new_species + self.name = f"OBTAIN_{data.species[new_species].name}" + + def offset_item_value(item_value: int) -> int: """ Returns the AP item id (code) for a given item value diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index ebd36898ab2..01ec76456a7 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald from dataclasses import dataclass from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, - PerGameCommonOptions, OptionGroup, StartInventory, OptionList) + PerGameCommonOptions, OptionGroup, StartInventory) from .data import data @@ -129,14 +129,14 @@ class Dexsanity(Toggle): display_name = "Dexsanity" -class DexsanityEncounterTypes(OptionList): +class DexsanityEncounterTypes(OptionSet): """ Determines which Dexsanity encounter areas are in logic. Logic will only consider access to Pokemon at these encounter types, but they may still be found elsewhere. """ display_name = "Dexsanity Encounter Types" - valid_keys = {"Land", "Water", "Fishing"} + valid_keys = ["Land", "Water", "Fishing"] default = valid_keys.copy() diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index d51b80f3530..7638756a610 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -4,8 +4,9 @@ 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, EncounterType, EncounterTableData, LearnsetMove, SpeciesData, +from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, PokemonSource, EncounterTableData, LearnsetMove, SpeciesData, MapData, data) +from .items import PokemonEmeraldObtainPokemonEventItem from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, TmTutorCompatibility) @@ -227,14 +228,14 @@ 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"}, +_encounter_subcategory_ranges: Dict[PokemonSource, Dict[range, Optional[str]]] = { + PokemonSource.LAND: {range(0, 12): None}, + PokemonSource.WATER: {range(0, 5): None}, + PokemonSource.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): +def _adjust_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slots: List[int], encounter_type: PokemonSource): """ Renames the events that correspond to wild encounters to reflect the new species there after randomization """ @@ -254,22 +255,17 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo # 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}" + encounter_location_name = f"{map_data.name}_{encounter_type}_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}" + assert isinstance(slot_location.item, PokemonEmeraldObtainPokemonEventItem) + slot_location.item.replace_species(new_species_id) except KeyError: pass # Map probably isn't included; should be careful here about bad encounter location names def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: - encounter_table = { - "Land": EncounterType.LAND, - "Water": EncounterType.WATER, - "Fishing": EncounterType.FISHING, - } - enabled_encounters = {encounter_table[encounter_type] for encounter_type in world.options.dexsanity_encounter_types.value} if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: return @@ -296,7 +292,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: placed_priority_species = False map_data = world.modified_maps[map_name] - new_encounters: Dict[EncounterType, EncounterTableData] = {} + new_encounters: Dict[PokemonSource, EncounterTableData] = {} for encounter_type, table in map_data.encounters.items(): # Create a map from the original species to new species @@ -306,7 +302,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: 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: + and encounter_type != PokemonSource.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS: new_species_id = priority_species.pop() placed_priority_species = True else: @@ -370,7 +366,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: species_old_to_new_map[species_id] = new_species_id - if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \ + if world.options.dexsanity and encounter_type != PokemonSource.ROCK_SMASH \ and map_name not in OUT_OF_LOGIC_MAPS and new_species_id not in world.blacklisted_wilds: already_placed.add(new_species_id) @@ -378,14 +374,14 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: new_slots: List[int] = [] for species_id in table.slots: new_slots.append(species_old_to_new_map[species_id]) - if encounter_type in enabled_encounters: + if encounter_type in world.enabled_dexsanity_encounter_types: world.allowed_dexsanity_species.update(new_slots) 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) + if encounter_type != PokemonSource.ROCK_SMASH: + _adjust_wild_events(world, map_data, new_slots, encounter_type) map_data.encounters = new_encounters diff --git a/worlds/pokemon_emerald/regions.py b/worlds/pokemon_emerald/regions.py index 36f3cb7e19a..357d80f5aa0 100644 --- a/worlds/pokemon_emerald/regions.py +++ b/worlds/pokemon_emerald/regions.py @@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple from BaseClasses import CollectionState, ItemClassification, Region -from .data import EncounterType, data -from .items import PokemonEmeraldItem +from .data import PokemonSource, data +from .items import PokemonEmeraldItem, PokemonEmeraldObtainPokemonEventItem from .locations import PokemonEmeraldLocation if TYPE_CHECKING: @@ -20,10 +20,10 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]: """ # 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. 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: [ + encounter_categories: Dict[PokemonSource, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = { + PokemonSource.LAND: [(None, range(0, 12), None)], + PokemonSource.WATER: [(None, range(0, 5), None)], + PokemonSource.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)), @@ -43,7 +43,7 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]: # For each of land, water, and fishing, connect the region if indicated by include_slots for i, (encounter_type, subcategories) in enumerate(encounter_categories.items()): if include_slots[i]: - region_name = f"{map_name}_{encounter_type.value}_ENCOUNTERS" + region_name = f"{map_name}_{encounter_type}_ENCOUNTERS" # If the region hasn't been created yet, create it now try: @@ -56,7 +56,7 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]: 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 = [] + unique_species: list[int] = [] for j, species_id in enumerate(encounter_slots): if j in subcategory[1] and not species_id in unique_species: unique_species.append(species_id) @@ -76,11 +76,10 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]: encounter_location.access_rule = subcategory[2] # Fill the location with an event for catching that species - encounter_location.place_locked_item(PokemonEmeraldItem( - f"CATCH_{data.species[species_id].name}", - ItemClassification.progression_skip_balancing, - None, - world.player + encounter_location.place_locked_item(PokemonEmeraldObtainPokemonEventItem( + world.player, + encounter_type, + species_id, )) encounter_region.locations.append(encounter_location) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index ce3d48ca154..47774225ef8 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -586,7 +586,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: entrance_rules["REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"] = Has("Mach Bike") # Route 134 - location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("CATCH_SPECIES_WAILORD", "CATCH_SPECIES_RELICANTH") + location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("REGI_WALL_SPECIES_WAILORD", "REGI_WALL_SPECIES_RELICANTH") entrance_rules["REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"] = hm_rules["HM08 Dive"] entrance_rules["REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"] = Has("EVENT_MOVE_TUTOR_DIG") @@ -637,7 +637,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species: continue - location_rules[f"Pokedex - {species.label}"] = Has(f"CATCH_{species.name}") + location_rules[f"Pokedex - {species.label}"] = Has(f"DEXSANITY_{species.name}") # Legendary hunt prevents Latios from being a wild spawn so the roamer # can be tracked, and also guarantees that the roamer is a Latios. diff --git a/worlds/pokemon_emerald/util.py b/worlds/pokemon_emerald/util.py index 268ba9bf5fa..3839a58a90c 100644 --- a/worlds/pokemon_emerald/util.py +++ b/worlds/pokemon_emerald/util.py @@ -1,7 +1,7 @@ import orjson from typing import Any, Dict, List, Optional, Tuple, Iterable -from .data import NATIONAL_ID_TO_SPECIES_ID, EncounterType, data +from .data import NATIONAL_ID_TO_SPECIES_ID, PokemonSource, data CHARACTER_DECODING_MAP = { @@ -86,8 +86,8 @@ 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: +def get_encounter_type_label(encounter_type: PokemonSource, slot: int) -> str: + if encounter_type == PokemonSource.FISHING: return { 0: "Old Rod", 1: "Old Rod", @@ -102,9 +102,9 @@ def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str: }[slot] return { - EncounterType.LAND: 'Land', - EncounterType.WATER: 'Water', - EncounterType.ROCK_SMASH: 'Rock Smash', + PokemonSource.LAND: 'Land', + PokemonSource.WATER: 'Water', + PokemonSource.ROCK_SMASH: 'Rock Smash', }[encounter_type]