Validate and regenerate all seed data from PokeDB

- Regenerate seed JSON for all 37 games with more complete PokeDB data
- Add category field to games.json (original/enhanced/remake/sequel/spinoff)
- Include all 1350 pokemon in pokemon.json with types and local sprites
- Build reverse index for PokeDB form lookups (types/sprites for evolutions)
- Move sprites to frontend/public/sprites, reference as /sprites/{id}.webp
- Truncate Sw/Sh den names to fit DB VARCHAR(100) limit
- Deduplicate route names and merge unnamed child areas into parent routes
- Populate 7 previously empty games (Sw/Sh, BDSP, PLA, Sc/Vi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-11 11:52:51 +01:00
parent df55233c62
commit 872d7872ce
47 changed files with 463655 additions and 129874 deletions

View File

@@ -302,6 +302,29 @@ _FORM_SUFFIX_MAP: dict[str, str] = {
}
# PokeDB type IDs → type names (from PokeDB's type system)
TYPE_ID_MAP: dict[int, str] = {
1: "normal",
2: "fighting",
3: "flying",
4: "poison",
5: "ground",
6: "rock",
7: "bug",
8: "ghost",
9: "steel",
10: "fire",
11: "water",
12: "grass",
13: "electric",
14: "psychic",
15: "ice",
16: "dragon",
17: "dark",
18: "fairy",
}
def _normalize_slug(identifier: str) -> str:
"""Normalize a PokeDB pokemon_form_identifier to a PokeAPI-style slug.
@@ -390,6 +413,7 @@ class PokemonMapper:
# 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._existing_types: dict[int, list[str]] = {} # pokeapi_id → types (fallback)
self._unmapped: set[str] = set()
if pokemon_json_path.exists():
@@ -401,6 +425,8 @@ class PokemonMapper:
name = p["name"]
ndex = p["national_dex"]
self._id_to_info[pid] = (ndex, name)
if p.get("types"):
self._existing_types[pid] = p["types"]
# Index by base slug (from pokeapi_id for base forms)
slug = _name_to_slug(name)
@@ -413,11 +439,37 @@ class PokemonMapper:
# Build index from PokeDB pokemon_forms.json if it has useful fields
self._pokedb_form_index: dict[str, dict] = {}
# Reverse index: pokeapi_id → PokeDB form record (for non-encountered lookups)
self._id_to_pokedb_form: dict[int, dict] = {}
for form in pokedb.pokemon_forms:
identifier = form.get("identifier", "")
if identifier:
self._pokedb_form_index[identifier] = form
# Build reverse index from pokeapi_id → PokeDB form
# First, for all encountered lookups that succeed, we cache the mapping.
# Here we pre-build for default forms using ndex_id.
for form in pokedb.pokemon_forms:
ndex = form.get("ndex_id")
if ndex and form.get("is_default_form"):
# Default form matches the base species (ndex == pokeapi_id for base forms)
if ndex in self._id_to_info:
self._id_to_pokedb_form[ndex] = form
# Also look for alternate-form pokeapi_ids that share the same ndex
for pid, (p_ndex, _) in self._id_to_info.items():
if p_ndex == ndex and pid not in self._id_to_pokedb_form:
self._id_to_pokedb_form[pid] = form
# Map non-default forms to their specific pokeapi_ids where possible
for form in pokedb.pokemon_forms:
identifier = form.get("identifier", "")
if not identifier or form.get("is_default_form"):
continue
slug = _normalize_slug(identifier)
if slug in self._slug_to_info:
pid, _ = self._slug_to_info[slug]
self._id_to_pokedb_form[pid] = form
def lookup(self, pokemon_form_identifier: str | None) -> tuple[int, str] | None:
"""Look up a PokeDB pokemon_form_identifier.
@@ -487,6 +539,64 @@ class PokemonMapper:
return None
return self._pokedb_form_index.get(pokemon_form_identifier)
def all_pokemon(self) -> list[tuple[int, tuple[int, str]]]:
"""Return all known pokemon as [(pokeapi_id, (national_dex, name)), ...].
Sourced from the existing pokemon.json.
"""
return sorted(self._id_to_info.items())
def get_types_for_id(self, pokeapi_id: int) -> list[str]:
"""Get types for a pokemon by pokeapi_id, looking up via PokeDB form data.
Falls back to existing types from pokemon.json if no PokeDB form found.
"""
form = self._find_form_for_id(pokeapi_id)
if form:
types = []
t1 = form.get("type_1_id")
t2 = form.get("type_2_id")
if t1 and t1 in TYPE_ID_MAP:
types.append(TYPE_ID_MAP[t1])
if t2 and t2 in TYPE_ID_MAP:
types.append(TYPE_ID_MAP[t2])
if types:
return types
# Fallback to existing types from pokemon.json
return self._existing_types.get(pokeapi_id, [])
def _find_form_for_id(self, pokeapi_id: int) -> dict | None:
"""Find the PokeDB form record for a pokeapi_id."""
# Check pre-built reverse index first
if pokeapi_id in self._id_to_pokedb_form:
return self._id_to_pokedb_form[pokeapi_id]
if pokeapi_id not in self._id_to_info:
return None
_, name = self._id_to_info[pokeapi_id]
slug = _name_to_slug(name)
for suffix in ["-default", ""]:
candidate = slug + suffix
if candidate in self._pokedb_form_index:
return self._pokedb_form_index[candidate]
form_slug = _name_to_form_slug(name)
if form_slug:
for suffix in ["-default", ""]:
candidate = form_slug + suffix
if candidate in self._pokedb_form_index:
return self._pokedb_form_index[candidate]
return None
def has_sprite_for_id(self, pokeapi_id: int) -> bool:
"""Check if a sprite exists for a pokemon by pokeapi_id."""
form = self._find_form_for_id(pokeapi_id)
return bool(form and form.get("main_image_normal_path_medium"))
def get_sprite_url_for_id(self, pokeapi_id: int) -> str | None:
"""Get the PokeDB CDN sprite URL for a pokemon by pokeapi_id."""
form = self._find_form_for_id(pokeapi_id)
return form.get("main_image_normal_path_medium") if form else None
def report_unmapped(self) -> None:
"""Print warnings for any unmapped identifiers."""
if self._unmapped: