Separate PokeAPI ID from national dex for correct form identification
Pokemon forms (e.g., Alolan Rattata) had their PokeAPI ID (10091) stored as national_dex, causing them to display incorrectly. This renames the unique identifier to pokeapi_id and adds a real national_dex field shared between forms and their base species, so Alolan Rattata correctly shows as #19. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# nuzlocke-tracker-g8zi
|
||||
title: 'Fix Pokemon form identification: separate PokeAPI ID from national dex'
|
||||
status: completed
|
||||
type: bug
|
||||
priority: high
|
||||
created_at: 2026-02-07T13:44:25Z
|
||||
updated_at: 2026-02-07T13:54:29Z
|
||||
blocking:
|
||||
- 6aje
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Pokemon forms (Alolan, Galarian, etc.) share a national dex number with their base species (e.g., Alolan Rattata is still national dex #19, same as regular Rattata). However, the current data model uses the PokeAPI internal ID (e.g., 10091 for Alolan Rattata) as the `national_dex` field. This is semantically wrong and causes:
|
||||
|
||||
1. **Wrong display**: Frontend shows "#10091" next to Alolan Rattata instead of "#19"
|
||||
2. **Broken sorting**: Forms sort at the end of the Pokemon list instead of next to their base species
|
||||
3. **Misleading data**: The field name implies a national Pokedex number but contains an internal API ID
|
||||
|
||||
## Current architecture
|
||||
|
||||
The `national_dex` field is deeply embedded as the unique Pokemon identifier:
|
||||
|
||||
- **Database**: `SmallInteger` column with `UNIQUE` constraint (`alembic/versions/03e5f186a9d5`)
|
||||
- **Models**: `pokemon.py` — `mapped_column(SmallInteger, unique=True)`
|
||||
- **Seeder**: `loader.py` — upsert conflict resolution on `national_dex`, builds `{national_dex: id}` mapping for linking encounters and evolutions
|
||||
- **Seed data**: `fetch_pokeapi.py` — uses PokeAPI pokemon ID as `national_dex` for both base species and forms
|
||||
- **API**: All CRUD operations key on `national_dex`, returned as `nationalDex` in JSON
|
||||
- **Frontend**: Displayed as `#nationalDex` in Pokemon selector, admin table, encounter modals
|
||||
|
||||
## Proposed fix
|
||||
|
||||
Add a `pokemon_id` (or similar) field as the true unique identifier (the PokeAPI pokemon ID), and keep `national_dex` as the real national dex number (shared between forms). This requires changes across every layer.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Add new migration: add `pokemon_id` column (unique, not null), change `national_dex` unique constraint to non-unique
|
||||
- [ ] Update Pokemon model to add `pokemon_id` field
|
||||
- [ ] Update seed data: `pokemon.json` entries get both `pokemon_id` (PokeAPI ID) and `national_dex` (real dex number, from species endpoint)
|
||||
- [ ] Update `fetch_pokeapi.py`: for forms, look up the species to get the real national dex, store PokeAPI ID separately
|
||||
- [ ] Update `loader.py`: upsert on `pokemon_id` instead of `national_dex`, update encounter/evolution linking
|
||||
- [ ] Update API schemas and endpoints to expose both fields
|
||||
- [ ] Update frontend to display real `national_dex` but use `pokemon_id` internally for uniqueness
|
||||
- [ ] Update encounter seed data to reference `pokemon_id` instead of `national_dex`
|
||||
|
||||
## Impact
|
||||
|
||||
Touches almost every layer: migration, model, seeder, API, frontend. Should be done before more forms are added (bean 6aje) to avoid migrating bad data.
|
||||
@@ -0,0 +1,53 @@
|
||||
"""rename national_dex to pokeapi_id, add real national_dex
|
||||
|
||||
Revision ID: e5f6a7b8c9d0
|
||||
Revises: d4e5f6a7b8c9
|
||||
Create Date: 2026-02-07 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e5f6a7b8c9d0'
|
||||
down_revision: Union[str, Sequence[str], None] = 'd4e5f6a7b8c9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Rename national_dex -> pokeapi_id and widen to Integer
|
||||
op.alter_column(
|
||||
'pokemon', 'national_dex',
|
||||
new_column_name='pokeapi_id',
|
||||
type_=sa.Integer(),
|
||||
existing_type=sa.SmallInteger(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
|
||||
# Add real national_dex column (shared between forms and base species)
|
||||
op.add_column(
|
||||
'pokemon',
|
||||
sa.Column('national_dex', sa.SmallInteger(), nullable=False, server_default='0'),
|
||||
)
|
||||
|
||||
# Populate national_dex = pokeapi_id for all existing rows
|
||||
# (correct for base species; forms will be fixed by re-seeding)
|
||||
op.execute('UPDATE pokemon SET national_dex = pokeapi_id')
|
||||
|
||||
# Remove the default now that all rows are populated
|
||||
op.alter_column('pokemon', 'national_dex', server_default=None)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('pokemon', 'national_dex')
|
||||
op.alter_column(
|
||||
'pokemon', 'pokeapi_id',
|
||||
new_column_name='national_dex',
|
||||
type_=sa.SmallInteger(),
|
||||
existing_type=sa.Integer(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
@@ -43,7 +43,7 @@ async def list_pokemon(
|
||||
total = (await session.execute(count_query)).scalar() or 0
|
||||
|
||||
# Get paginated items
|
||||
items_query = base_query.order_by(Pokemon.national_dex).offset(offset).limit(limit)
|
||||
items_query = base_query.order_by(Pokemon.national_dex, Pokemon.name).offset(offset).limit(limit)
|
||||
result = await session.execute(items_query)
|
||||
items = result.scalars().all()
|
||||
|
||||
@@ -67,11 +67,12 @@ async def bulk_import_pokemon(
|
||||
for item in items:
|
||||
try:
|
||||
existing = await session.execute(
|
||||
select(Pokemon).where(Pokemon.national_dex == item.national_dex)
|
||||
select(Pokemon).where(Pokemon.pokeapi_id == item.pokeapi_id)
|
||||
)
|
||||
pokemon = existing.scalar_one_or_none()
|
||||
|
||||
if pokemon is not None:
|
||||
pokemon.national_dex = item.national_dex
|
||||
pokemon.name = item.name
|
||||
pokemon.types = item.types
|
||||
if item.sprite_url is not None:
|
||||
@@ -82,7 +83,7 @@ async def bulk_import_pokemon(
|
||||
session.add(pokemon)
|
||||
created += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Dex #{item.national_dex} ({item.name}): {e}")
|
||||
errors.append(f"PokeAPI #{item.pokeapi_id} ({item.name}): {e}")
|
||||
|
||||
await session.commit()
|
||||
return BulkImportResult(created=created, updated=updated, errors=errors)
|
||||
@@ -93,12 +94,12 @@ async def create_pokemon(
|
||||
data: PokemonCreate, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
existing = await session.execute(
|
||||
select(Pokemon).where(Pokemon.national_dex == data.national_dex)
|
||||
select(Pokemon).where(Pokemon.pokeapi_id == data.pokeapi_id)
|
||||
)
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Pokemon with national dex #{data.national_dex} already exists",
|
||||
detail=f"Pokemon with PokeAPI ID #{data.pokeapi_id} already exists",
|
||||
)
|
||||
|
||||
pokemon = Pokemon(**data.model_dump())
|
||||
@@ -145,17 +146,17 @@ async def update_pokemon(
|
||||
raise HTTPException(status_code=404, detail="Pokemon not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if "national_dex" in update_data:
|
||||
if "pokeapi_id" in update_data:
|
||||
existing = await session.execute(
|
||||
select(Pokemon).where(
|
||||
Pokemon.national_dex == update_data["national_dex"],
|
||||
Pokemon.pokeapi_id == update_data["pokeapi_id"],
|
||||
Pokemon.id != pokemon_id,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Pokemon with national dex #{update_data['national_dex']} already exists",
|
||||
detail=f"Pokemon with PokeAPI ID #{update_data['pokeapi_id']} already exists",
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import SmallInteger, String
|
||||
from sqlalchemy import Integer, SmallInteger, String
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -9,7 +9,8 @@ class Pokemon(Base):
|
||||
__tablename__ = "pokemon"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
national_dex: Mapped[int] = mapped_column(SmallInteger, unique=True)
|
||||
pokeapi_id: Mapped[int] = mapped_column(Integer, unique=True)
|
||||
national_dex: Mapped[int] = mapped_column(SmallInteger)
|
||||
name: Mapped[str] = mapped_column(String(50))
|
||||
types: Mapped[list[str]] = mapped_column(ARRAY(String(20)))
|
||||
sprite_url: Mapped[str | None] = mapped_column(String(500))
|
||||
@@ -22,4 +23,4 @@ class Pokemon(Base):
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Pokemon(id={self.id}, name='{self.name}', dex={self.national_dex})>"
|
||||
return f"<Pokemon(id={self.id}, name='{self.name}', pokeapi_id={self.pokeapi_id}, dex={self.national_dex})>"
|
||||
|
||||
@@ -5,6 +5,7 @@ from app.schemas.base import CamelModel
|
||||
|
||||
class PokemonResponse(CamelModel):
|
||||
id: int
|
||||
pokeapi_id: int
|
||||
national_dex: int
|
||||
name: str
|
||||
types: list[str]
|
||||
@@ -47,6 +48,7 @@ class RouteEncounterDetailResponse(RouteEncounterResponse):
|
||||
|
||||
|
||||
class PokemonCreate(CamelModel):
|
||||
pokeapi_id: int
|
||||
national_dex: int
|
||||
name: str
|
||||
types: list[str]
|
||||
@@ -54,6 +56,7 @@ class PokemonCreate(CamelModel):
|
||||
|
||||
|
||||
class PokemonUpdate(CamelModel):
|
||||
pokeapi_id: int | None = None
|
||||
national_dex: int | None = None
|
||||
name: str | None = None
|
||||
types: list[str] | None = None
|
||||
@@ -76,6 +79,7 @@ class RouteEncounterUpdate(CamelModel):
|
||||
|
||||
|
||||
class BulkImportItem(BaseModel):
|
||||
pokeapi_id: int
|
||||
national_dex: int
|
||||
name: str
|
||||
types: list[str]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"order": 1,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 252,
|
||||
"pokeapi_id": 252,
|
||||
"pokemon_name": "treecko",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -12,7 +12,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 255,
|
||||
"pokeapi_id": 255,
|
||||
"pokemon_name": "torchic",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -20,7 +20,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 258,
|
||||
"pokeapi_id": 258,
|
||||
"pokemon_name": "mudkip",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -34,7 +34,7 @@
|
||||
"order": 2,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 345,
|
||||
"pokeapi_id": 345,
|
||||
"pokemon_name": "lileep",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -42,7 +42,7 @@
|
||||
"max_level": 20
|
||||
},
|
||||
{
|
||||
"national_dex": 347,
|
||||
"pokeapi_id": 347,
|
||||
"pokemon_name": "anorith",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -56,7 +56,7 @@
|
||||
"order": 3,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 360,
|
||||
"pokeapi_id": 360,
|
||||
"pokemon_name": "wynaut",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -70,7 +70,7 @@
|
||||
"order": 4,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 351,
|
||||
"pokeapi_id": 351,
|
||||
"pokemon_name": "castform",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -84,7 +84,7 @@
|
||||
"order": 5,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 374,
|
||||
"pokeapi_id": 374,
|
||||
"pokemon_name": "beldum",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"order": 1,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 1,
|
||||
"pokeapi_id": 1,
|
||||
"pokemon_name": "bulbasaur",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -12,7 +12,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 4,
|
||||
"pokeapi_id": 4,
|
||||
"pokemon_name": "charmander",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -20,7 +20,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 7,
|
||||
"pokeapi_id": 7,
|
||||
"pokemon_name": "squirtle",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -34,7 +34,7 @@
|
||||
"order": 2,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 129,
|
||||
"pokeapi_id": 129,
|
||||
"pokemon_name": "magikarp",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -48,7 +48,7 @@
|
||||
"order": 3,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 133,
|
||||
"pokeapi_id": 133,
|
||||
"pokemon_name": "eevee",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -62,7 +62,7 @@
|
||||
"order": 4,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 131,
|
||||
"pokeapi_id": 131,
|
||||
"pokemon_name": "lapras",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -70,7 +70,7 @@
|
||||
"max_level": 25
|
||||
},
|
||||
{
|
||||
"national_dex": 106,
|
||||
"pokeapi_id": 106,
|
||||
"pokemon_name": "hitmonlee",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -78,7 +78,7 @@
|
||||
"max_level": 25
|
||||
},
|
||||
{
|
||||
"national_dex": 107,
|
||||
"pokeapi_id": 107,
|
||||
"pokemon_name": "hitmonchan",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -92,7 +92,7 @@
|
||||
"order": 5,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 138,
|
||||
"pokeapi_id": 138,
|
||||
"pokemon_name": "omanyte",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -100,7 +100,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 140,
|
||||
"pokeapi_id": 140,
|
||||
"pokemon_name": "kabuto",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -108,7 +108,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 142,
|
||||
"pokeapi_id": 142,
|
||||
"pokemon_name": "aerodactyl",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -122,7 +122,7 @@
|
||||
"order": 6,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 175,
|
||||
"pokeapi_id": 175,
|
||||
"pokemon_name": "togepi",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"order": 1,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 1,
|
||||
"pokeapi_id": 1,
|
||||
"pokemon_name": "bulbasaur",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -12,7 +12,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 4,
|
||||
"pokeapi_id": 4,
|
||||
"pokemon_name": "charmander",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -20,7 +20,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 7,
|
||||
"pokeapi_id": 7,
|
||||
"pokemon_name": "squirtle",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -34,7 +34,7 @@
|
||||
"order": 2,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 129,
|
||||
"pokeapi_id": 129,
|
||||
"pokemon_name": "magikarp",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -48,7 +48,7 @@
|
||||
"order": 3,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 133,
|
||||
"pokeapi_id": 133,
|
||||
"pokemon_name": "eevee",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -62,7 +62,7 @@
|
||||
"order": 4,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 131,
|
||||
"pokeapi_id": 131,
|
||||
"pokemon_name": "lapras",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -70,7 +70,7 @@
|
||||
"max_level": 25
|
||||
},
|
||||
{
|
||||
"national_dex": 106,
|
||||
"pokeapi_id": 106,
|
||||
"pokemon_name": "hitmonlee",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -78,7 +78,7 @@
|
||||
"max_level": 25
|
||||
},
|
||||
{
|
||||
"national_dex": 107,
|
||||
"pokeapi_id": 107,
|
||||
"pokemon_name": "hitmonchan",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -92,7 +92,7 @@
|
||||
"order": 5,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 138,
|
||||
"pokeapi_id": 138,
|
||||
"pokemon_name": "omanyte",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -100,7 +100,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 140,
|
||||
"pokeapi_id": 140,
|
||||
"pokemon_name": "kabuto",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -108,7 +108,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 142,
|
||||
"pokeapi_id": 142,
|
||||
"pokemon_name": "aerodactyl",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -122,7 +122,7 @@
|
||||
"order": 6,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 175,
|
||||
"pokeapi_id": 175,
|
||||
"pokemon_name": "togepi",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"order": 1,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 252,
|
||||
"pokeapi_id": 252,
|
||||
"pokemon_name": "treecko",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -12,7 +12,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 255,
|
||||
"pokeapi_id": 255,
|
||||
"pokemon_name": "torchic",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -20,7 +20,7 @@
|
||||
"max_level": 5
|
||||
},
|
||||
{
|
||||
"national_dex": 258,
|
||||
"pokeapi_id": 258,
|
||||
"pokemon_name": "mudkip",
|
||||
"method": "starter",
|
||||
"encounter_rate": 100,
|
||||
@@ -34,7 +34,7 @@
|
||||
"order": 2,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 345,
|
||||
"pokeapi_id": 345,
|
||||
"pokemon_name": "lileep",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -42,7 +42,7 @@
|
||||
"max_level": 20
|
||||
},
|
||||
{
|
||||
"national_dex": 347,
|
||||
"pokeapi_id": 347,
|
||||
"pokemon_name": "anorith",
|
||||
"method": "fossil",
|
||||
"encounter_rate": 100,
|
||||
@@ -56,7 +56,7 @@
|
||||
"order": 3,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 360,
|
||||
"pokeapi_id": 360,
|
||||
"pokemon_name": "wynaut",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -70,7 +70,7 @@
|
||||
"order": 4,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 351,
|
||||
"pokeapi_id": 351,
|
||||
"pokemon_name": "castform",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
@@ -84,7 +84,7 @@
|
||||
"order": 5,
|
||||
"encounters": [
|
||||
{
|
||||
"national_dex": 374,
|
||||
"pokeapi_id": 374,
|
||||
"pokemon_name": "beldum",
|
||||
"method": "gift",
|
||||
"encounter_rate": 100,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -63,8 +63,8 @@ INCLUDED_METHODS = {
|
||||
"headbutt",
|
||||
}
|
||||
|
||||
# Collect all pokemon dex numbers across games
|
||||
all_pokemon_dex: set[int] = set()
|
||||
# Collect all pokemon PokeAPI IDs across games
|
||||
all_pokeapi_ids: set[int] = set()
|
||||
|
||||
|
||||
def clean_location_name(name: str) -> str:
|
||||
@@ -137,7 +137,7 @@ def get_encounters_for_area(area_id: int, version_name: str) -> list[dict]:
|
||||
|
||||
encounters.append({
|
||||
"pokemon_name": pokemon_name,
|
||||
"national_dex": dex_num,
|
||||
"pokeapi_id": dex_num,
|
||||
"method": method,
|
||||
"chance": enc["chance"],
|
||||
"min_level": enc["min_level"],
|
||||
@@ -152,10 +152,10 @@ def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]:
|
||||
agg: dict[tuple[int, str], dict] = {}
|
||||
|
||||
for enc in raw_encounters:
|
||||
key = (enc["national_dex"], enc["method"])
|
||||
key = (enc["pokeapi_id"], enc["method"])
|
||||
if key not in agg:
|
||||
agg[key] = {
|
||||
"national_dex": enc["national_dex"],
|
||||
"pokeapi_id": enc["pokeapi_id"],
|
||||
"pokemon_name": enc["pokemon_name"],
|
||||
"method": enc["method"],
|
||||
"encounter_rate": 0,
|
||||
@@ -184,7 +184,7 @@ def merge_special_encounters(routes: list[dict], special_data: dict[str, list[di
|
||||
|
||||
for location_name, encounters in special_data.items():
|
||||
for enc in encounters:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
all_pokeapi_ids.add(enc["pokeapi_id"])
|
||||
|
||||
if location_name in route_map:
|
||||
route_map[location_name]["encounters"].extend(encounters)
|
||||
@@ -252,7 +252,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
|
||||
if aggregated:
|
||||
route_name = f"{display_name} ({area_suffix})"
|
||||
for enc in aggregated:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
all_pokeapi_ids.add(enc["pokeapi_id"])
|
||||
child_routes.append({
|
||||
"name": route_name,
|
||||
"order": 0,
|
||||
@@ -275,7 +275,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
|
||||
if aggregated:
|
||||
route_name = f"{display_name} ({area_suffix})"
|
||||
for enc in aggregated:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
all_pokeapi_ids.add(enc["pokeapi_id"])
|
||||
routes.append({
|
||||
"name": route_name,
|
||||
"order": 0,
|
||||
@@ -287,7 +287,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
|
||||
aggregated = aggregate_encounters(all_encounters)
|
||||
if aggregated:
|
||||
for enc in aggregated:
|
||||
all_pokemon_dex.add(enc["national_dex"])
|
||||
all_pokeapi_ids.add(enc["pokeapi_id"])
|
||||
routes.append({
|
||||
"name": display_name,
|
||||
"order": 0,
|
||||
@@ -351,7 +351,7 @@ def fetch_all_pokemon() -> list[dict]:
|
||||
all_species.append(dex)
|
||||
|
||||
# Also include form IDs that appear in encounter data
|
||||
form_ids = sorted(d for d in all_pokemon_dex if d >= 10000)
|
||||
form_ids = sorted(d for d in all_pokeapi_ids if d >= 10000)
|
||||
|
||||
all_species.sort()
|
||||
print(f"\n--- Fetching {len(all_species)} Pokemon species + {len(form_ids)} forms ---")
|
||||
@@ -363,6 +363,7 @@ def fetch_all_pokemon() -> list[dict]:
|
||||
poke = load_resource("pokemon", dex)
|
||||
types = [t["type"]["name"] for t in poke["types"]]
|
||||
pokemon_list.append({
|
||||
"pokeapi_id": dex,
|
||||
"national_dex": dex,
|
||||
"name": poke["name"].title().replace("-", " "),
|
||||
"types": types,
|
||||
@@ -375,8 +376,10 @@ def fetch_all_pokemon() -> list[dict]:
|
||||
for form_dex in form_ids:
|
||||
poke = load_resource("pokemon", form_dex)
|
||||
types = [t["type"]["name"] for t in poke["types"]]
|
||||
species_id = extract_id(poke["species"]["url"])
|
||||
pokemon_list.append({
|
||||
"national_dex": form_dex,
|
||||
"pokeapi_id": form_dex,
|
||||
"national_dex": species_id,
|
||||
"name": format_form_name(poke),
|
||||
"types": types,
|
||||
"sprite_url": f"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{form_dex}.png",
|
||||
@@ -385,7 +388,7 @@ def fetch_all_pokemon() -> list[dict]:
|
||||
if form_ids:
|
||||
print(f" Fetched {len(form_ids)} forms")
|
||||
|
||||
return sorted(pokemon_list, key=lambda x: x["national_dex"])
|
||||
return sorted(pokemon_list, key=lambda x: (x["national_dex"], x["pokeapi_id"]))
|
||||
|
||||
|
||||
def flatten_evolution_chain(chain: dict, seeded_dex: set[int]) -> list[dict]:
|
||||
@@ -442,8 +445,8 @@ def flatten_evolution_chain(chain: dict, seeded_dex: set[int]) -> list[dict]:
|
||||
|
||||
if from_dex in seeded_dex and to_dex in seeded_dex:
|
||||
pairs.append({
|
||||
"from_national_dex": from_dex,
|
||||
"to_national_dex": to_dex,
|
||||
"from_pokeapi_id": from_dex,
|
||||
"to_pokeapi_id": to_dex,
|
||||
"trigger": trigger,
|
||||
"min_level": min_level,
|
||||
"item": item,
|
||||
@@ -484,13 +487,13 @@ def fetch_evolution_data(seeded_dex: set[int]) -> list[dict]:
|
||||
chain = load_resource("evolution-chain", chain_id)
|
||||
pairs = flatten_evolution_chain(chain["chain"], seeded_dex)
|
||||
for p in pairs:
|
||||
key = (p["from_national_dex"], p["to_national_dex"], p["trigger"])
|
||||
key = (p["from_pokeapi_id"], p["to_pokeapi_id"], p["trigger"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
all_pairs.append(p)
|
||||
|
||||
print(f" Total evolution pairs: {len(all_pairs)}")
|
||||
return sorted(all_pairs, key=lambda x: (x["from_national_dex"], x["to_national_dex"]))
|
||||
return sorted(all_pairs, key=lambda x: (x["from_pokeapi_id"], x["to_pokeapi_id"]))
|
||||
|
||||
|
||||
def apply_evolution_overrides(evolutions: list[dict]) -> None:
|
||||
@@ -506,15 +509,15 @@ def apply_evolution_overrides(evolutions: list[dict]) -> None:
|
||||
for removal in overrides.get("remove", []):
|
||||
evolutions[:] = [
|
||||
e for e in evolutions
|
||||
if not (e["from_national_dex"] == removal["from_dex"]
|
||||
and e["to_national_dex"] == removal["to_dex"])
|
||||
if not (e["from_pokeapi_id"] == removal["from_dex"]
|
||||
and e["to_pokeapi_id"] == removal["to_dex"])
|
||||
]
|
||||
|
||||
# Add entries
|
||||
for addition in overrides.get("add", []):
|
||||
evolutions.append({
|
||||
"from_national_dex": addition["from_dex"],
|
||||
"to_national_dex": addition["to_dex"],
|
||||
"from_pokeapi_id": addition["from_dex"],
|
||||
"to_pokeapi_id": addition["to_dex"],
|
||||
"trigger": addition.get("trigger", "level-up"),
|
||||
"min_level": addition.get("min_level"),
|
||||
"item": addition.get("item"),
|
||||
@@ -525,13 +528,13 @@ def apply_evolution_overrides(evolutions: list[dict]) -> None:
|
||||
# Modify entries
|
||||
for mod in overrides.get("modify", []):
|
||||
for e in evolutions:
|
||||
if (e["from_national_dex"] == mod["from_dex"]
|
||||
and e["to_national_dex"] == mod["to_dex"]):
|
||||
if (e["from_pokeapi_id"] == mod["from_dex"]
|
||||
and e["to_pokeapi_id"] == mod["to_dex"]):
|
||||
for key, value in mod.get("set", {}).items():
|
||||
e[key] = value
|
||||
|
||||
# Re-sort
|
||||
evolutions.sort(key=lambda x: (x["from_national_dex"], x["to_national_dex"]))
|
||||
evolutions.sort(key=lambda x: (x["from_pokeapi_id"], x["to_pokeapi_id"]))
|
||||
print(f" Applied overrides: {len(evolutions)} pairs after overrides")
|
||||
|
||||
|
||||
@@ -580,8 +583,8 @@ def main():
|
||||
write_json("pokemon.json", pokemon)
|
||||
print(f"\nWrote {len(pokemon)} Pokemon to pokemon.json")
|
||||
|
||||
# Build set of all seeded dex numbers for evolution filtering
|
||||
all_seeded_dex = {p["national_dex"] for p in pokemon}
|
||||
# Build set of all seeded PokeAPI IDs for evolution filtering
|
||||
all_seeded_dex = {p["pokeapi_id"] for p in pokemon}
|
||||
|
||||
# Fetch evolution chains for all seeded Pokemon
|
||||
evolutions = fetch_evolution_data(all_seeded_dex)
|
||||
|
||||
@@ -40,16 +40,18 @@ async def upsert_games(session: AsyncSession, games: list[dict]) -> dict[str, in
|
||||
|
||||
|
||||
async def upsert_pokemon(session: AsyncSession, pokemon_list: list[dict]) -> dict[int, int]:
|
||||
"""Upsert pokemon records, return {national_dex: id} mapping."""
|
||||
"""Upsert pokemon records, return {pokeapi_id: id} mapping."""
|
||||
for poke in pokemon_list:
|
||||
stmt = insert(Pokemon).values(
|
||||
pokeapi_id=poke["pokeapi_id"],
|
||||
national_dex=poke["national_dex"],
|
||||
name=poke["name"],
|
||||
types=poke["types"],
|
||||
sprite_url=poke.get("sprite_url"),
|
||||
).on_conflict_do_update(
|
||||
index_elements=["national_dex"],
|
||||
index_elements=["pokeapi_id"],
|
||||
set_={
|
||||
"national_dex": poke["national_dex"],
|
||||
"name": poke["name"],
|
||||
"types": poke["types"],
|
||||
"sprite_url": poke.get("sprite_url"),
|
||||
@@ -59,8 +61,8 @@ async def upsert_pokemon(session: AsyncSession, pokemon_list: list[dict]) -> dic
|
||||
|
||||
await session.flush()
|
||||
|
||||
result = await session.execute(select(Pokemon.national_dex, Pokemon.id))
|
||||
return {row.national_dex: row.id for row in result}
|
||||
result = await session.execute(select(Pokemon.pokeapi_id, Pokemon.id))
|
||||
return {row.pokeapi_id: row.id for row in result}
|
||||
|
||||
|
||||
async def upsert_routes(
|
||||
@@ -131,9 +133,9 @@ async def upsert_route_encounters(
|
||||
"""Upsert encounters for a route, return count of upserted rows."""
|
||||
count = 0
|
||||
for enc in encounters:
|
||||
pokemon_id = dex_to_id.get(enc["national_dex"])
|
||||
pokemon_id = dex_to_id.get(enc["pokeapi_id"])
|
||||
if pokemon_id is None:
|
||||
print(f" Warning: no pokemon_id for dex {enc['national_dex']}")
|
||||
print(f" Warning: no pokemon_id for pokeapi_id {enc['pokeapi_id']}")
|
||||
continue
|
||||
|
||||
stmt = insert(RouteEncounter).values(
|
||||
@@ -169,8 +171,8 @@ async def upsert_evolutions(
|
||||
|
||||
count = 0
|
||||
for evo in evolutions:
|
||||
from_id = dex_to_id.get(evo["from_national_dex"])
|
||||
to_id = dex_to_id.get(evo["to_national_dex"])
|
||||
from_id = dex_to_id.get(evo["from_pokeapi_id"])
|
||||
to_id = dex_to_id.get(evo["to_pokeapi_id"])
|
||||
if from_id is None or to_id is None:
|
||||
continue
|
||||
|
||||
|
||||
@@ -9,79 +9,79 @@ same format as aggregated PokeAPI encounters.
|
||||
SPECIAL_ENCOUNTERS: dict[str, dict[str, list[dict]]] = {
|
||||
"firered-leafgreen": {
|
||||
"Starter": [
|
||||
{"national_dex": 1, "pokemon_name": "bulbasaur", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 4, "pokemon_name": "charmander", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 7, "pokemon_name": "squirtle", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 1, "pokemon_name": "bulbasaur", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 4, "pokemon_name": "charmander", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 7, "pokemon_name": "squirtle", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Route 4": [
|
||||
{"national_dex": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Celadon City": [
|
||||
{"national_dex": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
],
|
||||
"Saffron City": [
|
||||
{"national_dex": 131, "pokemon_name": "lapras", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"national_dex": 106, "pokemon_name": "hitmonlee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"national_dex": 107, "pokemon_name": "hitmonchan", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"pokeapi_id": 131, "pokemon_name": "lapras", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"pokeapi_id": 106, "pokemon_name": "hitmonlee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"pokeapi_id": 107, "pokemon_name": "hitmonchan", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
],
|
||||
"Cinnabar Island": [
|
||||
{"national_dex": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Water Labyrinth": [
|
||||
{"national_dex": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
},
|
||||
"heartgold-soulsilver": {
|
||||
"Starter": [
|
||||
{"national_dex": 152, "pokemon_name": "chikorita", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 155, "pokemon_name": "cyndaquil", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 158, "pokemon_name": "totodile", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 152, "pokemon_name": "chikorita", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 155, "pokemon_name": "cyndaquil", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 158, "pokemon_name": "totodile", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Violet City": [
|
||||
{"national_dex": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1},
|
||||
{"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1},
|
||||
],
|
||||
"Goldenrod City": [
|
||||
{"national_dex": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Cianwood City": [
|
||||
{"national_dex": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
],
|
||||
"Mt Mortar": [
|
||||
{"national_dex": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10},
|
||||
{"pokeapi_id": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10},
|
||||
],
|
||||
"Dragons Den": [
|
||||
{"national_dex": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15},
|
||||
{"pokeapi_id": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15},
|
||||
],
|
||||
"Pewter City": [
|
||||
{"national_dex": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 408, "pokemon_name": "cranidos", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 410, "pokemon_name": "shieldon", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 408, "pokemon_name": "cranidos", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 410, "pokemon_name": "shieldon", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
],
|
||||
},
|
||||
"emerald": {
|
||||
"Starter": [
|
||||
{"national_dex": 252, "pokemon_name": "treecko", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 255, "pokemon_name": "torchic", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"national_dex": 258, "pokemon_name": "mudkip", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 252, "pokemon_name": "treecko", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 255, "pokemon_name": "torchic", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 258, "pokemon_name": "mudkip", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Route 119": [
|
||||
{"national_dex": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
{"pokeapi_id": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
|
||||
],
|
||||
"Lavaridge Town": [
|
||||
{"national_dex": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Mossdeep City": [
|
||||
{"national_dex": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
{"pokeapi_id": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
|
||||
],
|
||||
"Rustboro City": [
|
||||
{"national_dex": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"national_dex": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
{"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const updatePokemon = (id: number, data: UpdatePokemonInput) =>
|
||||
export const deletePokemon = (id: number) =>
|
||||
api.del(`/pokemon/${id}`)
|
||||
|
||||
export const bulkImportPokemon = (items: Array<{ nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) =>
|
||||
export const bulkImportPokemon = (items: Array<{ pokeapiId: number; nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) =>
|
||||
api.post<BulkImportResult>('/pokemon/bulk-import', items)
|
||||
|
||||
// Evolutions
|
||||
|
||||
@@ -2,13 +2,13 @@ import { type FormEvent, useState } from 'react'
|
||||
import type { BulkImportResult } from '../../types'
|
||||
|
||||
interface BulkImportModalProps {
|
||||
onSubmit: (items: Array<{ nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) => Promise<BulkImportResult>
|
||||
onSubmit: (items: Array<{ pokeapiId: number; nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) => Promise<BulkImportResult>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const EXAMPLE = `[
|
||||
{ "nationalDex": 1, "name": "Bulbasaur", "types": ["Grass", "Poison"] },
|
||||
{ "nationalDex": 4, "name": "Charmander", "types": ["Fire"] }
|
||||
{ "pokeapiId": 1, "nationalDex": 1, "name": "Bulbasaur", "types": ["Grass", "Poison"] },
|
||||
{ "pokeapiId": 4, "nationalDex": 4, "name": "Charmander", "types": ["Fire"] }
|
||||
]`
|
||||
|
||||
export function BulkImportModal({ onSubmit, onClose }: BulkImportModalProps) {
|
||||
@@ -33,7 +33,7 @@ export function BulkImportModal({ onSubmit, onClose }: BulkImportModalProps) {
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const res = await onSubmit(items as Array<{ nationalDex: number; name: string; types: string[] }>)
|
||||
const res = await onSubmit(items as Array<{ pokeapiId: number; nationalDex: number; name: string; types: string[] }>)
|
||||
setResult(res)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Import failed')
|
||||
|
||||
@@ -10,6 +10,7 @@ interface PokemonFormModalProps {
|
||||
}
|
||||
|
||||
export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting }: PokemonFormModalProps) {
|
||||
const [pokeapiId, setPokeapiId] = useState(String(pokemon?.pokeapiId ?? ''))
|
||||
const [nationalDex, setNationalDex] = useState(String(pokemon?.nationalDex ?? ''))
|
||||
const [name, setName] = useState(pokemon?.name ?? '')
|
||||
const [types, setTypes] = useState(pokemon?.types.join(', ') ?? '')
|
||||
@@ -22,6 +23,7 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting }: P
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
onSubmit({
|
||||
pokeapiId: Number(pokeapiId),
|
||||
nationalDex: Number(nationalDex),
|
||||
name,
|
||||
types: typesList,
|
||||
@@ -36,6 +38,17 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting }: P
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">PokeAPI ID</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
value={pokeapiId}
|
||||
onChange={(e) => setPokeapiId(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">National Dex #</label>
|
||||
<input
|
||||
|
||||
@@ -160,7 +160,7 @@ export function useDeletePokemon() {
|
||||
export function useBulkImportPokemon() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (items: Array<{ nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) =>
|
||||
mutationFn: (items: Array<{ pokeapiId: number; nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) =>
|
||||
adminApi.bulkImportPokemon(items),
|
||||
onSuccess: (result) => {
|
||||
qc.invalidateQueries({ queryKey: ['pokemon'] })
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface RouteReorderItem {
|
||||
}
|
||||
|
||||
export interface CreatePokemonInput {
|
||||
pokeapiId: number
|
||||
nationalDex: number
|
||||
name: string
|
||||
types: string[]
|
||||
@@ -39,6 +40,7 @@ export interface CreatePokemonInput {
|
||||
}
|
||||
|
||||
export interface UpdatePokemonInput {
|
||||
pokeapiId?: number
|
||||
nationalDex?: number
|
||||
name?: string
|
||||
types?: string[]
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface RouteWithChildren extends Route {
|
||||
|
||||
export interface Pokemon {
|
||||
id: number
|
||||
pokeapiId: number
|
||||
nationalDex: number
|
||||
name: string
|
||||
types: string[]
|
||||
|
||||
Reference in New Issue
Block a user