"""Reference data mappings: PokeDB identifiers → seed format values.""" from __future__ import annotations import json import re import sys from pathlib import Path from typing import Any from .loader import PokeDBData # --------------------------------------------------------------------------- # Encounter method mapping # --------------------------------------------------------------------------- # PokeDB encounter_method_identifier → our simplified method name. # Keys can be exact matches or prefix patterns (ending with *). ENCOUNTER_METHOD_MAP: dict[str, str] = { # Walking / grass / cave "walking-tall-grass": "walk", "walking-long-grass": "walk", "walking-cave": "walk", "walking-bridge": "walk", "walking-building": "walk", "walking-sand": "walk", "walking-snow": "walk", "walking-rough-terrain": "walk", "walking-marsh": "walk", "walking-puddle": "walk", "walking-flower-field": "walk", "walking-ice": "walk", "walking-forest": "walk", "walking-snowfield": "walk", "dark-grass": "walk", "shaking-grass": "walk", "rustling-grass": "walk", "yellow-flowers": "walk", "red-flowers": "walk", "purple-flowers": "walk", # Surfing "surfing": "surf", "surfing-ocean": "surf", "surfing-puddle": "surf", "surfing-rapids": "surf", "surfing-underwater": "surf", "rippling-water": "surf", # Fishing "fishing-old-rod": "old-rod", "fishing-good-rod": "good-rod", "fishing-super-rod": "super-rod", "fishing": "fishing", "fishing-special": "fishing", # Rock smash "rock-smash": "rock-smash", # Headbutt "headbutt-low": "headbutt", "headbutt-normal": "headbutt", "headbutt-high": "headbutt", # Gift / special acquisition "npc-gift": "gift", "egg": "gift", "revive": "gift", "fossil": "gift", # Trade "npc-trade": "trade", # Overworld / symbol encounters (Gen 8+) "symbol-encounter": "walk", "wanderer": "walk", "flying": "walk", # Static / fixed "fixed-encounter": "static", "static-encounter": "static", "legendary-encounter": "static", "interactable": "static", # Special methods "swarm": "swarm", "poke-radar": "pokeradar", "dual-slot-mode": "dual-slot", "honey-tree": "honey", "trophy-garden": "walk", "great-marsh": "walk", "cave-spot": "walk", "bubble-spot": "surf", "sand-spot": "walk", "horde": "walk", "sos-encounter": "walk", "ambush": "walk", # Seaweed / diving "diving": "surf", "seaweed": "surf", # Raids "max-raid": "raid", "dynamax-adventure": "raid", "tera-raid": "raid", # Misc "roaming": "roaming", "safari-zone": "walk", "bug-contest": "walk", } # Prefix-based fallbacks for methods not explicitly listed above. _METHOD_PREFIX_MAP: list[tuple[str, str]] = [ ("walking-", "walk"), ("surfing-", "surf"), ("fishing-", "fishing"), ("headbutt-", "headbutt"), ("flying-", "walk"), ] def map_encounter_method(method_identifier: str) -> str | None: """Map a PokeDB encounter method to our simplified method name. Returns None if the method is unrecognized. """ if method_identifier in ENCOUNTER_METHOD_MAP: return ENCOUNTER_METHOD_MAP[method_identifier] for prefix, mapped in _METHOD_PREFIX_MAP: if method_identifier.startswith(prefix): return mapped return None # --------------------------------------------------------------------------- # Version mapping # --------------------------------------------------------------------------- # PokeDB version identifiers that differ from our game slugs. # Most are 1:1, these handle exceptions. _VERSION_OVERRIDES: dict[str, str] = { "lets-go-pikachu": "lets-go-pikachu", "lets-go-eevee": "lets-go-eevee", } def build_version_map( pokedb: PokeDBData, version_groups: dict[str, Any], ) -> dict[str, str]: """Build a mapping from PokeDB version_identifier → our game slug. Returns the mapping dict. Logs warnings for unmapped versions. """ # Collect all our known game slugs our_slugs: set[str] = set() for vg in version_groups.values(): for slug in vg.get("versions", []): our_slugs.add(slug) # Collect all PokeDB version identifiers pokedb_versions: set[str] = set() for v in pokedb.versions: identifier = v.get("identifier", "") if identifier: pokedb_versions.add(identifier) mapping: dict[str, str] = {} for pdb_ver in sorted(pokedb_versions): if pdb_ver in _VERSION_OVERRIDES: mapping[pdb_ver] = _VERSION_OVERRIDES[pdb_ver] elif pdb_ver in our_slugs: mapping[pdb_ver] = pdb_ver # else: PokeDB version not in our version_groups (expected for some) # Report our games that have no PokeDB mapping mapped_slugs = set(mapping.values()) unmapped_ours = our_slugs - mapped_slugs if unmapped_ours: print(f" Versions in our config with no PokeDB match: {sorted(unmapped_ours)}") return mapping # --------------------------------------------------------------------------- # Pokemon form mapping # --------------------------------------------------------------------------- def _normalize_slug(identifier: str) -> str: """Normalize a PokeDB pokemon_form_identifier to a PokeAPI-style slug. PokeDB uses "pidgey-default" for base forms — strip the "-default" suffix. Non-default forms like "rattata-alola" are already PokeAPI-style slugs. """ if identifier.endswith("-default"): return identifier[: -len("-default")] return identifier def _name_to_slug(name: str) -> str: """Convert a display name to a PokeAPI-style slug. "Bulbasaur" → "bulbasaur" "Mr. Mime" → "mr-mime" "Farfetch'd" → "farfetchd" "Nidoran♀" → "nidoran-f" "Nidoran♂" → "nidoran-m" "Flabébé" → "flabebe" "Type: Null" → "type-null" """ s = name.lower() s = s.replace("♀", "-f").replace("♂", "-m") s = s.replace("'", "").replace("'", "").replace(".", "").replace(":", "") s = s.replace("é", "e").replace("É", "e") s = s.replace(" ", "-") # Collapse multiple hyphens s = re.sub(r"-+", "-", s) return s.strip("-") def _name_to_form_slug(name: str) -> str | None: """Convert a display name with form suffix to a PokeAPI-style slug. "Rattata (Alola)" → "rattata-alola" "Basculin (Blue Striped)" → "basculin-blue-striped" "Deoxys Normal" → "deoxys-normal" (space-separated variant) """ # Try parenthesized form: "Base (Suffix)" m = re.match(r"^(.+?)\s*\((.+)\)$", name) if m: base = _name_to_slug(m.group(1)) suffix = _name_to_slug(m.group(2)) return f"{base}-{suffix}" # Try space-separated form: "Deoxys Normal" parts = name.split() if len(parts) >= 2: return _name_to_slug(name) return None class PokemonMapper: """Maps PokeDB pokemon_form_identifier → (pokeapi_id, display_name).""" def __init__(self, pokemon_json_path: Path, pokedb: PokeDBData) -> None: # Build slug → (pokeapi_id, name) from existing pokemon.json self._slug_to_info: dict[str, tuple[int, str]] = {} self._id_to_info: dict[int, tuple[int, str]] = {} # pokeapi_id → (national_dex, name) self._unmapped: set[str] = set() if pokemon_json_path.exists(): with open(pokemon_json_path) as f: pokemon_list = json.load(f) for p in pokemon_list: pid = p["pokeapi_id"] name = p["name"] ndex = p["national_dex"] self._id_to_info[pid] = (ndex, name) # Index by base slug (from pokeapi_id for base forms) slug = _name_to_slug(name) self._slug_to_info[slug] = (pid, name) # Also index by form slug if it has a form suffix form_slug = _name_to_form_slug(name) if form_slug and form_slug != slug: self._slug_to_info[form_slug] = (pid, name) # Build index from PokeDB pokemon_forms.json if it has useful fields self._pokedb_form_index: dict[str, dict] = {} for form in pokedb.pokemon_forms: identifier = form.get("identifier", "") if identifier: self._pokedb_form_index[identifier] = form def lookup(self, pokemon_form_identifier: str | None) -> tuple[int, str] | None: """Look up a PokeDB pokemon_form_identifier. Returns (pokeapi_id, display_name) or None if unmapped. """ if not pokemon_form_identifier: return None slug = _normalize_slug(pokemon_form_identifier) # Direct slug match if slug in self._slug_to_info: return self._slug_to_info[slug] # Try the PokeDB form record for a pokemon_id field form_record = self._pokedb_form_index.get(pokemon_form_identifier, {}) pokemon_id = form_record.get("pokemon_id") if pokemon_id and pokemon_id in self._id_to_info: ndex, name = self._id_to_info[pokemon_id] # Cache for future lookups self._slug_to_info[slug] = (pokemon_id, name) return (pokemon_id, name) # Track unmapped if pokemon_form_identifier not in self._unmapped: self._unmapped.add(pokemon_form_identifier) return None def report_unmapped(self) -> None: """Print warnings for any unmapped identifiers.""" if self._unmapped: print( f"\nWarning: {len(self._unmapped)} unmapped pokemon form identifiers:", file=sys.stderr, ) for ident in sorted(self._unmapped): print(f" - {ident}", file=sys.stderr) # --------------------------------------------------------------------------- # Location area mapping # --------------------------------------------------------------------------- # Region prefixes to strip from location identifiers (matching Go tool behavior). _REGION_PREFIXES = [ "kanto-", "johto-", "hoenn-", "sinnoh-", "unova-", "kalos-", "alola-", "galar-", "hisui-", "paldea-", ] def _identifier_to_name(identifier: str) -> str: """Convert a hyphenated identifier to a Title Case display name. "route-01-kanto" → "Route 01 Kanto" (region stripping done separately) "viridian-forest" → "Viridian Forest" """ return identifier.replace("-", " ").title() class LocationMapper: """Maps PokeDB location_area_identifier → (location_name, area_suffix).""" def __init__(self, pokedb: PokeDBData) -> None: # Build location_area_identifier → location_identifier lookup self._area_to_location: dict[str, str] = {} # location_identifier → location display name self._location_names: dict[str, str] = {} # location_area_identifier → area display name self._area_names: dict[str, str] = {} # Index locations for loc in pokedb.locations: identifier = loc.get("identifier", "") name = loc.get("name", "") if identifier: self._location_names[identifier] = name if name else self._clean_location_name(identifier) # Index location areas for area in pokedb.location_areas: area_id = area.get("identifier", "") loc_id = area.get("location_identifier", "") area_name = area.get("name", "") if area_id: self._area_to_location[area_id] = loc_id self._area_names[area_id] = area_name if area_name else "" @staticmethod def _clean_location_name(identifier: str) -> str: """Clean a location identifier into a display name.""" name = identifier for prefix in _REGION_PREFIXES: if name.startswith(prefix): name = name[len(prefix):] break return _identifier_to_name(name) def get_location_name(self, location_area_identifier: str) -> str: """Get the display name for a location area's parent location.""" loc_id = self._area_to_location.get(location_area_identifier, "") if loc_id and loc_id in self._location_names: return self._location_names[loc_id] # Fallback: derive from the area identifier itself return self._clean_location_name(location_area_identifier) def get_area_name(self, location_area_identifier: str) -> str: """Get the display name for a specific location area.""" return self._area_names.get(location_area_identifier, "") def get_location_identifier(self, location_area_identifier: str) -> str: """Get the parent location identifier for a location area.""" return self._area_to_location.get(location_area_identifier, "")