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:
@@ -3,15 +3,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .loader import SeedConfig
|
||||
from .mappings import PokemonMapper
|
||||
from .mappings import TYPE_ID_MAP, PokemonMapper
|
||||
from .models import Encounter, Route
|
||||
from .sprites import sprite_path_for_pokemon
|
||||
|
||||
# Max route name length (matches DB column VARCHAR(100))
|
||||
_MAX_ROUTE_NAME_LEN = 100
|
||||
|
||||
# Pattern for Sw/Sh den area names: "Location (Den X - long description - Common/Rare)"
|
||||
_DEN_NAME_RE = re.compile(r"^(.+?) \(Den ([A-Z]\d*) - .+ - (Common|Rare)\)$")
|
||||
|
||||
|
||||
def _truncate_route_name(name: str) -> str:
|
||||
"""Truncate a route name to fit the database column limit.
|
||||
|
||||
Applies smart truncation for known patterns (e.g. Sw/Sh den descriptions).
|
||||
"""
|
||||
if len(name) <= _MAX_ROUTE_NAME_LEN:
|
||||
return name
|
||||
|
||||
# Sw/Sh dens: shorten "Location (Den X - long desc - Common/Rare)" → "Location (Den X - Common/Rare)"
|
||||
m = _DEN_NAME_RE.match(name)
|
||||
if m:
|
||||
shortened = f"{m.group(1)} (Den {m.group(2)} - {m.group(3)})"
|
||||
if len(shortened) <= _MAX_ROUTE_NAME_LEN:
|
||||
return shortened
|
||||
|
||||
# Generic truncation: cut at last space before limit, add ellipsis
|
||||
truncated = name[:_MAX_ROUTE_NAME_LEN - 1].rsplit(" ", 1)[0] + "…"
|
||||
return truncated
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route ordering
|
||||
@@ -133,24 +160,66 @@ def write_json(path: Path, data: Any) -> None:
|
||||
print(f" -> {path}")
|
||||
|
||||
|
||||
def _deduplicate_names(routes: list[Route]) -> None:
|
||||
"""Ensure all route names are unique by appending a numeric suffix to duplicates."""
|
||||
seen: dict[str, int] = {}
|
||||
|
||||
def _unique(name: str) -> str:
|
||||
if name not in seen:
|
||||
seen[name] = 1
|
||||
return name
|
||||
seen[name] += 1
|
||||
return f"{name} #{seen[name]}"
|
||||
|
||||
for route in routes:
|
||||
route.name = _unique(_truncate_route_name(route.name))
|
||||
for child in route.children:
|
||||
child.name = _unique(_truncate_route_name(child.name))
|
||||
|
||||
|
||||
def write_game_json(routes: list[Route], output_dir: Path, game_slug: str) -> None:
|
||||
"""Write a per-game route/encounter JSON file."""
|
||||
_deduplicate_names(routes)
|
||||
data = [r.to_dict() for r in routes]
|
||||
write_json(output_dir / f"{game_slug}.json", data)
|
||||
|
||||
|
||||
_GAME_CATEGORY: dict[str, str] = {
|
||||
"red": "original", "blue": "original", "yellow": "enhanced",
|
||||
"gold": "original", "silver": "original", "crystal": "enhanced",
|
||||
"ruby": "original", "sapphire": "original", "emerald": "enhanced",
|
||||
"firered": "remake", "leafgreen": "remake",
|
||||
"diamond": "original", "pearl": "original", "platinum": "enhanced",
|
||||
"heartgold": "remake", "soulsilver": "remake",
|
||||
"black": "original", "white": "original",
|
||||
"black-2": "sequel", "white-2": "sequel",
|
||||
"x": "original", "y": "original",
|
||||
"omega-ruby": "remake", "alpha-sapphire": "remake",
|
||||
"sun": "original", "moon": "original",
|
||||
"ultra-sun": "enhanced", "ultra-moon": "enhanced",
|
||||
"lets-go-pikachu": "remake", "lets-go-eevee": "remake",
|
||||
"sword": "original", "shield": "original",
|
||||
"brilliant-diamond": "remake", "shining-pearl": "remake",
|
||||
"legends-arceus": "spinoff",
|
||||
"scarlet": "original", "violet": "original",
|
||||
"legends-z-a": "spinoff",
|
||||
}
|
||||
|
||||
|
||||
def write_games_json(config: SeedConfig, output_dir: Path) -> None:
|
||||
"""Write games.json from version_groups config."""
|
||||
games = []
|
||||
for vg_info in config.version_groups.values():
|
||||
for game_info in vg_info.get("games", {}).values():
|
||||
slug = game_info["slug"]
|
||||
games.append({
|
||||
"name": game_info["name"],
|
||||
"slug": game_info["slug"],
|
||||
"slug": slug,
|
||||
"generation": vg_info["generation"],
|
||||
"region": vg_info["region"],
|
||||
"release_year": game_info["release_year"],
|
||||
"color": game_info.get("color"),
|
||||
"category": _GAME_CATEGORY.get(slug, "original"),
|
||||
})
|
||||
write_json(output_dir / "games.json", games)
|
||||
print(f" Wrote {len(games)} games")
|
||||
@@ -162,31 +231,36 @@ def write_pokemon_json(
|
||||
sprite_map: dict[str, str],
|
||||
output_dir: Path,
|
||||
) -> None:
|
||||
"""Write pokemon.json with all pokemon referenced in encounters."""
|
||||
seen_ids: set[int] = set()
|
||||
pokemon_list: list[dict[str, Any]] = []
|
||||
"""Write pokemon.json with all known pokemon.
|
||||
|
||||
for form_id in sorted(encountered_form_ids):
|
||||
Includes all pokemon from the existing pokemon.json (base data),
|
||||
enriched with PokeDB types and sprite paths for encountered forms.
|
||||
"""
|
||||
# Build a mapping of pokeapi_id → (form_id, form_data) for encountered forms
|
||||
encountered_by_id: dict[int, tuple[str, dict[str, Any] | None]] = {}
|
||||
for form_id in encountered_form_ids:
|
||||
info = pokemon_mapper.lookup(form_id)
|
||||
if info is None:
|
||||
continue
|
||||
pokeapi_id, _ = info
|
||||
if pokeapi_id not in encountered_by_id:
|
||||
form_data = pokemon_mapper.get_form_data(form_id)
|
||||
encountered_by_id[pokeapi_id] = (form_id, form_data)
|
||||
|
||||
pokeapi_id, name = info
|
||||
if pokeapi_id in seen_ids:
|
||||
continue
|
||||
seen_ids.add(pokeapi_id)
|
||||
pokemon_list: list[dict[str, Any]] = []
|
||||
|
||||
# Get additional data from PokeDB form record
|
||||
form_data = pokemon_mapper.get_form_data(form_id)
|
||||
national_dex = form_data.get("ndex_id", pokeapi_id) if form_data else pokeapi_id
|
||||
|
||||
# Types from PokeDB form data
|
||||
types = _extract_types(form_data) if form_data else []
|
||||
|
||||
# Sprite URL
|
||||
sprite_url: str | None = None
|
||||
if form_id in sprite_map:
|
||||
sprite_url = sprite_path_for_pokemon(pokeapi_id)
|
||||
for pokeapi_id, (ndex, name) in pokemon_mapper.all_pokemon():
|
||||
# Enrich with PokeDB data if this pokemon was encountered
|
||||
if pokeapi_id in encountered_by_id:
|
||||
form_id, form_data = encountered_by_id[pokeapi_id]
|
||||
types = _extract_types(form_data) if form_data else []
|
||||
sprite_url = sprite_path_for_pokemon(pokeapi_id) if form_id in sprite_map else None
|
||||
national_dex = form_data.get("ndex_id", ndex) if form_data else ndex
|
||||
else:
|
||||
# Not encountered — use existing data, try to find PokeDB form for types
|
||||
types = pokemon_mapper.get_types_for_id(pokeapi_id)
|
||||
sprite_url = sprite_path_for_pokemon(pokeapi_id) if pokemon_mapper.has_sprite_for_id(pokeapi_id) else None
|
||||
national_dex = ndex
|
||||
|
||||
pokemon_list.append({
|
||||
"pokeapi_id": pokeapi_id,
|
||||
@@ -203,36 +277,13 @@ def write_pokemon_json(
|
||||
print(f" Wrote {len(pokemon_list)} pokemon")
|
||||
|
||||
|
||||
# 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 _extract_types(form_data: dict[str, Any]) -> list[str]:
|
||||
"""Extract type names from a PokeDB form record."""
|
||||
types = []
|
||||
type1_id = form_data.get("type_1_id")
|
||||
type2_id = form_data.get("type_2_id")
|
||||
if type1_id and type1_id in _TYPE_ID_MAP:
|
||||
types.append(_TYPE_ID_MAP[type1_id])
|
||||
if type2_id and type2_id in _TYPE_ID_MAP:
|
||||
types.append(_TYPE_ID_MAP[type2_id])
|
||||
if type1_id and type1_id in TYPE_ID_MAP:
|
||||
types.append(TYPE_ID_MAP[type1_id])
|
||||
if type2_id and type2_id in TYPE_ID_MAP:
|
||||
types.append(TYPE_ID_MAP[type2_id])
|
||||
return types
|
||||
|
||||
Reference in New Issue
Block a user