diff --git a/.beans/nuzlocke-tracker-f44d--add-pokemon-forms-support-to-seeding.md b/.beans/nuzlocke-tracker-f44d--add-pokemon-forms-support-to-seeding.md index 6147c1f..25cc170 100644 --- a/.beans/nuzlocke-tracker-f44d--add-pokemon-forms-support-to-seeding.md +++ b/.beans/nuzlocke-tracker-f44d--add-pokemon-forms-support-to-seeding.md @@ -1,10 +1,11 @@ --- # nuzlocke-tracker-f44d title: Add Pokemon forms support to seeding -status: todo +status: completed type: task +priority: normal created_at: 2026-02-06T10:11:23Z -updated_at: 2026-02-06T10:11:23Z +updated_at: 2026-02-07T13:30:57Z --- The current seeding only fetches base Pokemon species. It should also include alternate forms (Alolan, Galarian, Mega, regional variants, etc.) which have different types and stats. diff --git a/backend/src/app/seeds/data/platinum.json b/backend/src/app/seeds/data/platinum.json index 00c9a18..e507b94 100644 --- a/backend/src/app/seeds/data/platinum.json +++ b/backend/src/app/seeds/data/platinum.json @@ -85,71 +85,9 @@ } ] }, - { - "name": "Eterna City", - "order": 2, - "encounters": [ - { - "national_dex": 129, - "pokemon_name": "magikarp", - "method": "old-rod", - "encounter_rate": 100, - "min_level": 3, - "max_level": 15 - }, - { - "national_dex": 54, - "pokemon_name": "psyduck", - "method": "surf", - "encounter_rate": 90, - "min_level": 20, - "max_level": 30 - }, - { - "national_dex": 130, - "pokemon_name": "gyarados", - "method": "super-rod", - "encounter_rate": 55, - "min_level": 30, - "max_level": 55 - }, - { - "national_dex": 129, - "pokemon_name": "magikarp", - "method": "good-rod", - "encounter_rate": 55, - "min_level": 10, - "max_level": 25 - }, - { - "national_dex": 339, - "pokemon_name": "barboach", - "method": "good-rod", - "encounter_rate": 45, - "min_level": 10, - "max_level": 25 - }, - { - "national_dex": 340, - "pokemon_name": "whiscash", - "method": "super-rod", - "encounter_rate": 45, - "min_level": 30, - "max_level": 55 - }, - { - "national_dex": 55, - "pokemon_name": "golduck", - "method": "surf", - "encounter_rate": 10, - "min_level": 20, - "max_level": 40 - } - ] - }, { "name": "Pastoria City", - "order": 3, + "order": 2, "encounters": [ { "national_dex": 129, @@ -235,7 +173,7 @@ }, { "name": "Sunyshore City", - "order": 4, + "order": 3, "encounters": [ { "national_dex": 129, @@ -321,7 +259,7 @@ }, { "name": "Pokemon League", - "order": 5, + "order": 4, "encounters": [ { "national_dex": 129, @@ -399,12 +337,12 @@ }, { "name": "Oreburgh Mine", - "order": 6, + "order": 5, "encounters": [], "children": [ { "name": "Oreburgh Mine (1F)", - "order": 7, + "order": 6, "encounters": [ { "national_dex": 74, @@ -434,7 +372,7 @@ }, { "name": "Oreburgh Mine (B1F)", - "order": 8, + "order": 7, "encounters": [ { "national_dex": 74, @@ -466,7 +404,7 @@ }, { "name": "Valley Windworks", - "order": 9, + "order": 8, "encounters": [ { "national_dex": 129, @@ -600,7 +538,7 @@ }, { "name": "Eterna Forest", - "order": 10, + "order": 9, "encounters": [ { "national_dex": 406, @@ -756,6 +694,68 @@ } ] }, + { + "name": "Eterna City", + "order": 10, + "encounters": [ + { + "national_dex": 129, + "pokemon_name": "magikarp", + "method": "old-rod", + "encounter_rate": 100, + "min_level": 3, + "max_level": 15 + }, + { + "national_dex": 54, + "pokemon_name": "psyduck", + "method": "surf", + "encounter_rate": 90, + "min_level": 20, + "max_level": 30 + }, + { + "national_dex": 130, + "pokemon_name": "gyarados", + "method": "super-rod", + "encounter_rate": 55, + "min_level": 30, + "max_level": 55 + }, + { + "national_dex": 129, + "pokemon_name": "magikarp", + "method": "good-rod", + "encounter_rate": 55, + "min_level": 10, + "max_level": 25 + }, + { + "national_dex": 339, + "pokemon_name": "barboach", + "method": "good-rod", + "encounter_rate": 45, + "min_level": 10, + "max_level": 25 + }, + { + "national_dex": 340, + "pokemon_name": "whiscash", + "method": "super-rod", + "encounter_rate": 45, + "min_level": 30, + "max_level": 55 + }, + { + "national_dex": 55, + "pokemon_name": "golduck", + "method": "surf", + "encounter_rate": 10, + "min_level": 20, + "max_level": 40 + } + ] + }, { "name": "Fuego Ironworks", "order": 11, diff --git a/backend/src/app/seeds/data/pokemon.json b/backend/src/app/seeds/data/pokemon.json index 66c7f3b..ef27615 100644 --- a/backend/src/app/seeds/data/pokemon.json +++ b/backend/src/app/seeds/data/pokemon.json @@ -8724,5 +8724,145 @@ "ghost" ], "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1025.png" + }, + { + "national_dex": 10016, + "name": "Basculin (Blue Striped)", + "types": [ + "water" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10016.png" + }, + { + "national_dex": 10091, + "name": "Rattata (Alola)", + "types": [ + "dark", + "normal" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10091.png" + }, + { + "national_dex": 10092, + "name": "Raticate (Alola)", + "types": [ + "dark", + "normal" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10092.png" + }, + { + "national_dex": 10101, + "name": "Sandshrew (Alola)", + "types": [ + "ice", + "steel" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10101.png" + }, + { + "national_dex": 10103, + "name": "Vulpix (Alola)", + "types": [ + "ice" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10103.png" + }, + { + "national_dex": 10105, + "name": "Diglett (Alola)", + "types": [ + "ground", + "steel" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10105.png" + }, + { + "national_dex": 10106, + "name": "Dugtrio (Alola)", + "types": [ + "ground", + "steel" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10106.png" + }, + { + "national_dex": 10107, + "name": "Meowth (Alola)", + "types": [ + "dark" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10107.png" + }, + { + "national_dex": 10109, + "name": "Geodude (Alola)", + "types": [ + "rock", + "electric" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10109.png" + }, + { + "national_dex": 10110, + "name": "Graveler (Alola)", + "types": [ + "rock", + "electric" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10110.png" + }, + { + "national_dex": 10112, + "name": "Grimer (Alola)", + "types": [ + "poison", + "dark" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10112.png" + }, + { + "national_dex": 10114, + "name": "Exeggutor (Alola)", + "types": [ + "grass", + "dragon" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10114.png" + }, + { + "national_dex": 10123, + "name": "Oricorio (Pom Pom)", + "types": [ + "electric", + "flying" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10123.png" + }, + { + "national_dex": 10124, + "name": "Oricorio (Pau)", + "types": [ + "psychic", + "flying" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10124.png" + }, + { + "national_dex": 10125, + "name": "Oricorio (Sensu)", + "types": [ + "ghost", + "flying" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10125.png" + }, + { + "national_dex": 10126, + "name": "Lycanroc (Midnight)", + "types": [ + "rock" + ], + "sprite_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/10126.png" } ] \ No newline at end of file diff --git a/backend/src/app/seeds/fetch_pokeapi.py b/backend/src/app/seeds/fetch_pokeapi.py index e4b1c98..9233341 100644 --- a/backend/src/app/seeds/fetch_pokeapi.py +++ b/backend/src/app/seeds/fetch_pokeapi.py @@ -19,7 +19,8 @@ from app.seeds.special_encounters import SPECIAL_ENCOUNTERS REPO_ROOT = Path(__file__).parents[4] # backend/src/app/seeds -> repo root POKEAPI_DIR = REPO_ROOT / "data" / "pokeapi" / "data" / "api" / "v2" -DATA_DIR = Path(__file__).parent / "data" +SEEDS_DIR = Path(__file__).parent +DATA_DIR = SEEDS_DIR / "data" def load_resource(endpoint: str, resource_id: int) -> dict: @@ -704,6 +705,170 @@ ROUTE_ORDER: dict[str, list[str]] = { "Artisan Cave", "Altering Cave", ], + "platinum": [ + "Canalave City", + "Pastoria City", + "Sunyshore City", + "Pokemon League", + "Oreburgh Mine", + "Oreburgh Mine (1F)", + "Oreburgh Mine (B1F)", + "Valley Windworks", + "Eterna Forest", + "Eterna City", + "Fuego Ironworks", + "Mt Coronet", + "Mt Coronet (1F Route 207)", + "Mt Coronet (2F)", + "Mt Coronet (3F)", + "Mt Coronet (Exterior Snowfall)", + "Mt Coronet (Exterior Blizzard)", + "Mt Coronet (4F)", + "Mt Coronet (4F Small Room)", + "Mt Coronet (5F)", + "Mt Coronet (6F)", + "Mt Coronet (1F From Exterior)", + "Mt Coronet (1F Route 216)", + "Mt Coronet (1F Route 211)", + "Mt Coronet (B1F)", + "Great Marsh", + "Great Marsh (Area 1)", + "Great Marsh (Area 2)", + "Great Marsh (Area 3)", + "Great Marsh (Area 4)", + "Great Marsh (Area 5)", + "Great Marsh (Area 6)", + "Solaceon Ruins", + "Solaceon Ruins (2F)", + "Solaceon Ruins (1F)", + "Solaceon Ruins (B1F A)", + "Solaceon Ruins (B1F B)", + "Solaceon Ruins (B1F C)", + "Solaceon Ruins (B2F A)", + "Solaceon Ruins (B2F B)", + "Solaceon Ruins (B2F C)", + "Solaceon Ruins (B3F A)", + "Solaceon Ruins (B3F B)", + "Solaceon Ruins (B3F C)", + "Solaceon Ruins (B3F D)", + "Solaceon Ruins (B3F E)", + "Solaceon Ruins (B4F A)", + "Solaceon Ruins (B4F B)", + "Solaceon Ruins (B4F C)", + "Solaceon Ruins (B4F D)", + "Solaceon Ruins (B5F)", + "Victory Road", + "Victory Road (1F)", + "Victory Road (2F)", + "Victory Road (B1F)", + "Victory Road (Inside B1F)", + "Victory Road (Inside)", + "Victory Road (Inside Exit)", + "Ravaged Path", + "Oreburgh Gate", + "Oreburgh Gate (1F)", + "Oreburgh Gate (B1F)", + "Stark Mountain (Entrance)", + "Stark Mountain (Inside)", + "Stark Mountain", + "Turnback Cave", + "Turnback Cave (Pillar 1)", + "Turnback Cave (Pillar 2)", + "Turnback Cave (Pillar 3)", + "Turnback Cave (Before Pillar 1)", + "Turnback Cave (Between Pillars 1 And 2)", + "Turnback Cave (Between Pillars 2 And 3)", + "Turnback Cave (After Pillar 3)", + "Snowpoint Temple", + "Snowpoint Temple (1F)", + "Snowpoint Temple (B1F)", + "Snowpoint Temple (B2F)", + "Snowpoint Temple (B3F)", + "Snowpoint Temple (B4F)", + "Snowpoint Temple (B5F)", + "Wayward Cave", + "Wayward Cave (1F)", + "Wayward Cave (B1F)", + "Ruin Maniac Cave", + "Ruin Maniac Cave (0 9 Different Unown Caught)", + "Ruin Maniac Cave (10 25 Different Unown Caught)", + "Trophy Garden", + "Iron Island (1F)", + "Iron Island (B1F Left)", + "Iron Island (B1F Right)", + "Iron Island (B2F Right)", + "Iron Island (B2F Left)", + "Iron Island (B3F)", + "Iron Island", + "Old Chateau", + "Old Chateau (Entrance)", + "Old Chateau (Dining Room)", + "Old Chateau (2F Private Room)", + "Old Chateau (2F)", + "Old Chateau (2F Leftmost Room)", + "Old Chateau (2F Left Room)", + "Old Chateau (2F Middle Room)", + "Old Chateau (2F Right Room)", + "Old Chateau (2F Rightmost Room)", + "Lake Verity", + "Lake Verity (Before Galactic Intervention)", + "Lake Verity (After Galactic Intervention)", + "Lake Valor", + "Lake Acuity", + "Valor Lakefront", + "Acuity Lakefront", + "Route 201", + "Route 202", + "Route 203", + "Route 204", + "Route 204 (South Towards Jubilife City)", + "Route 204 (North Towards Floaroma Town)", + "Route 205", + "Route 205 (South Towards Floaroma Town)", + "Route 205 (East Towards Eterna City)", + "Route 206", + "Route 207", + "Route 208", + "Route 209", + "Lost Tower", + "Lost Tower (1F)", + "Lost Tower (2F)", + "Lost Tower (3F)", + "Lost Tower (4F)", + "Lost Tower (5F)", + "Route 210", + "Route 210 (South Towards Solaceon Town)", + "Route 210 (West Towards Celestic Town)", + "Route 211", + "Route 211 (West Towards Eterna City)", + "Route 211 (East Towards Celestic Town)", + "Route 212", + "Route 212 (North Towards Hearthome City)", + "Route 212 (East Towards Pastoria City)", + "Route 213", + "Route 214", + "Route 215", + "Route 216", + "Route 217", + "Route 218", + "Route 219", + "Route 221", + "Route 222", + "Route 224", + "Route 225", + "Route 227", + "Route 228", + "Route 229", + "Twinleaf Town", + "Celestic Town", + "Resort Area", + "Sea Route 220", + "Sea Route 223", + "Sea Route 226", + "Sea Route 230", + "Sendoff Spring", + "Maniac Tunnel" + ] } # Aliases — version groups sharing same route progression @@ -975,22 +1140,42 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict] return routes +def format_form_name(poke_data: dict) -> str: + """Convert a PokeAPI pokemon form entry to a clean display name. + + e.g. 'rattata-alola' (species: 'rattata') -> 'Rattata (Alola)' + """ + species_name = poke_data["species"]["name"] + full_name = poke_data["name"] + if full_name.startswith(species_name + "-"): + form_suffix = full_name[len(species_name) + 1:] + base = species_name.title().replace("-", " ") + suffix = form_suffix.title().replace("-", " ") + return f"{base} ({suffix})" + return full_name.title().replace("-", " ") + + def fetch_all_pokemon() -> list[dict]: - """Fetch all Pokemon species from the local PokeAPI data.""" + """Fetch all Pokemon species + encountered forms from the local PokeAPI data.""" pokemon_dir = POKEAPI_DIR / "pokemon-species" - # Get all species IDs (directories with numeric names, excluding forms 10000+) + # Get all base species IDs all_species = [] for entry in pokemon_dir.iterdir(): if entry.is_dir() and entry.name.isdigit(): dex = int(entry.name) - if dex < 10000: # Exclude alternate forms + if dex < 10000: 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) + all_species.sort() - print(f"\n--- Fetching {len(all_species)} Pokemon species ---") + print(f"\n--- Fetching {len(all_species)} Pokemon species + {len(form_ids)} forms ---") pokemon_list = [] + + # Fetch base species for i, dex in enumerate(all_species, 1): poke = load_resource("pokemon", dex) types = [t["type"]["name"] for t in poke["types"]] @@ -1000,9 +1185,22 @@ def fetch_all_pokemon() -> list[dict]: "types": types, "sprite_url": f"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{dex}.png", }) - if i % 100 == 0 or i == len(all_species): - print(f" Fetched {i}/{len(all_species)}") + print(f" Fetched {i}/{len(all_species)} species") + + # Fetch forms (from pokemon endpoint, with form-aware naming) + for form_dex in form_ids: + poke = load_resource("pokemon", form_dex) + types = [t["type"]["name"] for t in poke["types"]] + pokemon_list.append({ + "national_dex": form_dex, + "name": format_form_name(poke), + "types": types, + "sprite_url": f"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{form_dex}.png", + }) + + if form_ids: + print(f" Fetched {len(form_ids)} forms") return sorted(pokemon_list, key=lambda x: x["national_dex"]) @@ -1081,8 +1279,9 @@ def fetch_evolution_data(seeded_dex: set[int]) -> list[dict]: print(f"\n--- Fetching evolution chains ---") # First, get the evolution chain URL for each pokemon species + # Skip form IDs (>= 10000) — they don't have pokemon-species entries chain_ids: set[int] = set() - dex_sorted = sorted(seeded_dex) + dex_sorted = sorted(d for d in seeded_dex if d < 10000) for i, dex in enumerate(dex_sorted, 1): species = load_resource("pokemon-species", dex)