Pokemon forms with dex IDs >= 10000 (e.g., Alolan Rattata = 10091) were being collected in encounter data but missing from pokemon.json, causing them to be silently dropped during DB seeding. Now fetch_all_pokemon() also fetches form entries that appear in encounter data, with clean display names like "Rattata (Alola)" and correct form-specific types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1415 lines
43 KiB
Python
1415 lines
43 KiB
Python
"""Fetch game data from local PokeAPI submodule and write static JSON seed files.
|
|
|
|
Reads from the PokeAPI/api-data git submodule at data/pokeapi/ — no network
|
|
access or container needed. Only uses Python stdlib.
|
|
|
|
Usage:
|
|
python -m app.seeds.fetch_pokeapi
|
|
|
|
Requires the submodule to be initialized:
|
|
git submodule update --init
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
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"
|
|
SEEDS_DIR = Path(__file__).parent
|
|
DATA_DIR = SEEDS_DIR / "data"
|
|
|
|
|
|
def load_resource(endpoint: str, resource_id: int) -> dict:
|
|
"""Load a PokeAPI resource from the local submodule data."""
|
|
path = POKEAPI_DIR / endpoint / str(resource_id) / "index.json"
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def extract_id(url: str) -> int:
|
|
"""Extract the numeric ID from a PokeAPI URL (absolute or relative)."""
|
|
return int(url.rstrip("/").split("/")[-1])
|
|
|
|
|
|
# Game definitions - Gen 1 through Gen 8
|
|
VERSION_GROUPS = {
|
|
# === Generation 1 ===
|
|
"red-blue": {
|
|
"versions": ["red", "blue"],
|
|
"generation": 1,
|
|
"region": "kanto",
|
|
"region_id": 1,
|
|
"games": {
|
|
"red": {
|
|
"name": "Pokemon Red",
|
|
"slug": "red",
|
|
"release_year": 1996,
|
|
"color": "#FF1111",
|
|
},
|
|
"blue": {
|
|
"name": "Pokemon Blue",
|
|
"slug": "blue",
|
|
"release_year": 1996,
|
|
"color": "#1111FF",
|
|
},
|
|
},
|
|
},
|
|
"yellow": {
|
|
"versions": ["yellow"],
|
|
"generation": 1,
|
|
"region": "kanto",
|
|
"region_id": 1,
|
|
"games": {
|
|
"yellow": {
|
|
"name": "Pokemon Yellow",
|
|
"slug": "yellow",
|
|
"release_year": 1998,
|
|
"color": "#FFD733",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 2 ===
|
|
"gold-silver": {
|
|
"versions": ["gold", "silver"],
|
|
"generation": 2,
|
|
"region": "johto",
|
|
"region_id": 2,
|
|
"extra_regions": [1], # Kanto post-game
|
|
"games": {
|
|
"gold": {
|
|
"name": "Pokemon Gold",
|
|
"slug": "gold",
|
|
"release_year": 1999,
|
|
"color": "#DAA520",
|
|
},
|
|
"silver": {
|
|
"name": "Pokemon Silver",
|
|
"slug": "silver",
|
|
"release_year": 1999,
|
|
"color": "#C0C0C0",
|
|
},
|
|
},
|
|
},
|
|
"crystal": {
|
|
"versions": ["crystal"],
|
|
"generation": 2,
|
|
"region": "johto",
|
|
"region_id": 2,
|
|
"extra_regions": [1], # Kanto post-game
|
|
"games": {
|
|
"crystal": {
|
|
"name": "Pokemon Crystal",
|
|
"slug": "crystal",
|
|
"release_year": 2000,
|
|
"color": "#4FD9FF",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 3 ===
|
|
"ruby-sapphire": {
|
|
"versions": ["ruby", "sapphire"],
|
|
"generation": 3,
|
|
"region": "hoenn",
|
|
"region_id": 3,
|
|
"games": {
|
|
"ruby": {
|
|
"name": "Pokemon Ruby",
|
|
"slug": "ruby",
|
|
"release_year": 2002,
|
|
"color": "#A00000",
|
|
},
|
|
"sapphire": {
|
|
"name": "Pokemon Sapphire",
|
|
"slug": "sapphire",
|
|
"release_year": 2002,
|
|
"color": "#0000A0",
|
|
},
|
|
},
|
|
},
|
|
"emerald": {
|
|
"versions": ["emerald"],
|
|
"generation": 3,
|
|
"region": "hoenn",
|
|
"region_id": 3,
|
|
"games": {
|
|
"emerald": {
|
|
"name": "Pokemon Emerald",
|
|
"slug": "emerald",
|
|
"release_year": 2005,
|
|
"color": "#00A000",
|
|
},
|
|
},
|
|
},
|
|
"firered-leafgreen": {
|
|
"versions": ["firered", "leafgreen"],
|
|
"generation": 3,
|
|
"region": "kanto",
|
|
"region_id": 1,
|
|
"games": {
|
|
"firered": {
|
|
"name": "Pokemon FireRed",
|
|
"slug": "firered",
|
|
"release_year": 2004,
|
|
"color": "#FF7327",
|
|
},
|
|
"leafgreen": {
|
|
"name": "Pokemon LeafGreen",
|
|
"slug": "leafgreen",
|
|
"release_year": 2004,
|
|
"color": "#00DD00",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 4 ===
|
|
"diamond-pearl": {
|
|
"versions": ["diamond", "pearl"],
|
|
"generation": 4,
|
|
"region": "sinnoh",
|
|
"region_id": 4,
|
|
"games": {
|
|
"diamond": {
|
|
"name": "Pokemon Diamond",
|
|
"slug": "diamond",
|
|
"release_year": 2006,
|
|
"color": "#AAAAFF",
|
|
},
|
|
"pearl": {
|
|
"name": "Pokemon Pearl",
|
|
"slug": "pearl",
|
|
"release_year": 2006,
|
|
"color": "#FFAAAA",
|
|
},
|
|
},
|
|
},
|
|
"platinum": {
|
|
"versions": ["platinum"],
|
|
"generation": 4,
|
|
"region": "sinnoh",
|
|
"region_id": 4,
|
|
"games": {
|
|
"platinum": {
|
|
"name": "Pokemon Platinum",
|
|
"slug": "platinum",
|
|
"release_year": 2008,
|
|
"color": "#999999",
|
|
},
|
|
},
|
|
},
|
|
"heartgold-soulsilver": {
|
|
"versions": ["heartgold", "soulsilver"],
|
|
"generation": 4,
|
|
"region": "johto",
|
|
"region_id": 2,
|
|
"extra_regions": [1], # Kanto post-game
|
|
"games": {
|
|
"heartgold": {
|
|
"name": "Pokemon HeartGold",
|
|
"slug": "heartgold",
|
|
"release_year": 2010,
|
|
"color": "#B69E00",
|
|
},
|
|
"soulsilver": {
|
|
"name": "Pokemon SoulSilver",
|
|
"slug": "soulsilver",
|
|
"release_year": 2010,
|
|
"color": "#C0C0E0",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 5 ===
|
|
"black-white": {
|
|
"versions": ["black", "white"],
|
|
"generation": 5,
|
|
"region": "unova",
|
|
"region_id": 5,
|
|
"games": {
|
|
"black": {
|
|
"name": "Pokemon Black",
|
|
"slug": "black",
|
|
"release_year": 2010,
|
|
"color": "#444444",
|
|
},
|
|
"white": {
|
|
"name": "Pokemon White",
|
|
"slug": "white",
|
|
"release_year": 2010,
|
|
"color": "#E1E1E1",
|
|
},
|
|
},
|
|
},
|
|
"black-2-white-2": {
|
|
"versions": ["black-2", "white-2"],
|
|
"generation": 5,
|
|
"region": "unova",
|
|
"region_id": 5,
|
|
"games": {
|
|
"black-2": {
|
|
"name": "Pokemon Black 2",
|
|
"slug": "black-2",
|
|
"release_year": 2012,
|
|
"color": "#424B50",
|
|
},
|
|
"white-2": {
|
|
"name": "Pokemon White 2",
|
|
"slug": "white-2",
|
|
"release_year": 2012,
|
|
"color": "#E3CED0",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 6 ===
|
|
"x-y": {
|
|
"versions": ["x", "y"],
|
|
"generation": 6,
|
|
"region": "kalos",
|
|
"region_id": 6,
|
|
"games": {
|
|
"x": {
|
|
"name": "Pokemon X",
|
|
"slug": "x",
|
|
"release_year": 2013,
|
|
"color": "#025DA6",
|
|
},
|
|
"y": {
|
|
"name": "Pokemon Y",
|
|
"slug": "y",
|
|
"release_year": 2013,
|
|
"color": "#EA1A3E",
|
|
},
|
|
},
|
|
},
|
|
"omega-ruby-alpha-sapphire": {
|
|
"versions": ["omega-ruby", "alpha-sapphire"],
|
|
"generation": 6,
|
|
"region": "hoenn",
|
|
"region_id": 3,
|
|
"games": {
|
|
"omega-ruby": {
|
|
"name": "Pokemon Omega Ruby",
|
|
"slug": "omega-ruby",
|
|
"release_year": 2014,
|
|
"color": "#CF3025",
|
|
},
|
|
"alpha-sapphire": {
|
|
"name": "Pokemon Alpha Sapphire",
|
|
"slug": "alpha-sapphire",
|
|
"release_year": 2014,
|
|
"color": "#26649C",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 7 ===
|
|
"sun-moon": {
|
|
"versions": ["sun", "moon"],
|
|
"generation": 7,
|
|
"region": "alola",
|
|
"region_id": 7,
|
|
"games": {
|
|
"sun": {
|
|
"name": "Pokemon Sun",
|
|
"slug": "sun",
|
|
"release_year": 2016,
|
|
"color": "#F1912B",
|
|
},
|
|
"moon": {
|
|
"name": "Pokemon Moon",
|
|
"slug": "moon",
|
|
"release_year": 2016,
|
|
"color": "#5599CA",
|
|
},
|
|
},
|
|
},
|
|
"ultra-sun-ultra-moon": {
|
|
"versions": ["ultra-sun", "ultra-moon"],
|
|
"generation": 7,
|
|
"region": "alola",
|
|
"region_id": 7,
|
|
"games": {
|
|
"ultra-sun": {
|
|
"name": "Pokemon Ultra Sun",
|
|
"slug": "ultra-sun",
|
|
"release_year": 2017,
|
|
"color": "#E95B2B",
|
|
},
|
|
"ultra-moon": {
|
|
"name": "Pokemon Ultra Moon",
|
|
"slug": "ultra-moon",
|
|
"release_year": 2017,
|
|
"color": "#204E8C",
|
|
},
|
|
},
|
|
},
|
|
"lets-go": {
|
|
"versions": ["lets-go-pikachu", "lets-go-eevee"],
|
|
"generation": 7,
|
|
"region": "kanto",
|
|
"region_id": 1,
|
|
"games": {
|
|
"lets-go-pikachu": {
|
|
"name": "Pokemon Let's Go Pikachu",
|
|
"slug": "lets-go-pikachu",
|
|
"release_year": 2018,
|
|
"color": "#F5DA00",
|
|
},
|
|
"lets-go-eevee": {
|
|
"name": "Pokemon Let's Go Eevee",
|
|
"slug": "lets-go-eevee",
|
|
"release_year": 2018,
|
|
"color": "#D4924B",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 8 ===
|
|
"sword-shield": {
|
|
"versions": ["sword", "shield"],
|
|
"generation": 8,
|
|
"region": "galar",
|
|
"region_id": 8,
|
|
"games": {
|
|
"sword": {
|
|
"name": "Pokemon Sword",
|
|
"slug": "sword",
|
|
"release_year": 2019,
|
|
"color": "#00D4E7",
|
|
},
|
|
"shield": {
|
|
"name": "Pokemon Shield",
|
|
"slug": "shield",
|
|
"release_year": 2019,
|
|
"color": "#EF3B6E",
|
|
},
|
|
},
|
|
},
|
|
"brilliant-diamond-shining-pearl": {
|
|
"versions": ["brilliant-diamond", "shining-pearl"],
|
|
"generation": 8,
|
|
"region": "sinnoh",
|
|
"region_id": 4,
|
|
"games": {
|
|
"brilliant-diamond": {
|
|
"name": "Pokemon Brilliant Diamond",
|
|
"slug": "brilliant-diamond",
|
|
"release_year": 2021,
|
|
"color": "#44BAE5",
|
|
},
|
|
"shining-pearl": {
|
|
"name": "Pokemon Shining Pearl",
|
|
"slug": "shining-pearl",
|
|
"release_year": 2021,
|
|
"color": "#E18AAA",
|
|
},
|
|
},
|
|
},
|
|
"legends-arceus": {
|
|
"versions": ["legends-arceus"],
|
|
"generation": 8,
|
|
"region": "hisui",
|
|
"region_id": 9,
|
|
"games": {
|
|
"legends-arceus": {
|
|
"name": "Pokemon Legends: Arceus",
|
|
"slug": "legends-arceus",
|
|
"release_year": 2022,
|
|
"color": "#36597B",
|
|
},
|
|
},
|
|
},
|
|
# === Generation 9 ===
|
|
"scarlet-violet": {
|
|
"versions": ["scarlet", "violet"],
|
|
"generation": 9,
|
|
"region": "paldea",
|
|
"region_id": 10,
|
|
"games": {
|
|
"scarlet": {
|
|
"name": "Pokemon Scarlet",
|
|
"slug": "scarlet",
|
|
"release_year": 2022,
|
|
"color": "#F93C3C",
|
|
},
|
|
"violet": {
|
|
"name": "Pokemon Violet",
|
|
"slug": "violet",
|
|
"release_year": 2022,
|
|
"color": "#A96EEC",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
# Encounter methods to include (excludes gift, legendary-only, etc.)
|
|
INCLUDED_METHODS = {
|
|
"walk",
|
|
"surf",
|
|
"old-rod",
|
|
"good-rod",
|
|
"super-rod",
|
|
"rock-smash",
|
|
"headbutt",
|
|
}
|
|
|
|
# Route progression order by version group.
|
|
# Keys are version group names from VERSION_GROUPS. Names must match
|
|
# clean_location_name() output. Routes not listed fall to the end alphabetically.
|
|
ROUTE_ORDER: dict[str, list[str]] = {
|
|
"firered-leafgreen": [
|
|
# Main Kanto progression
|
|
"Starter",
|
|
"Pallet Town",
|
|
"Route 1",
|
|
"Viridian City",
|
|
"Route 22",
|
|
"Route 2",
|
|
"Viridian Forest",
|
|
"Pewter City",
|
|
"Route 3",
|
|
"Mt Moon",
|
|
"Route 4",
|
|
"Cerulean City",
|
|
"Route 24",
|
|
"Route 25",
|
|
"Route 5",
|
|
"Route 6",
|
|
"Vermilion City",
|
|
"Ss Anne",
|
|
"Route 11",
|
|
"Digletts Cave",
|
|
"Route 9",
|
|
"Route 10",
|
|
"Rock Tunnel",
|
|
"Power Plant",
|
|
"Route 8",
|
|
"Route 7",
|
|
"Celadon City",
|
|
"Saffron City",
|
|
"Lavender Town",
|
|
"Route 16",
|
|
"Route 17",
|
|
"Route 18",
|
|
"Fuchsia City",
|
|
"Safari Zone",
|
|
"Route 15",
|
|
"Route 14",
|
|
"Route 13",
|
|
"Route 12",
|
|
"Pokemon Tower",
|
|
"Sea Route 19",
|
|
"Sea Route 20",
|
|
"Seafoam Islands",
|
|
"Cinnabar Island",
|
|
"Pokemon Mansion",
|
|
"Sea Route 21",
|
|
"Route 23",
|
|
"Victory Road 2",
|
|
"Cerulean Cave",
|
|
# Sevii Islands
|
|
"One Island",
|
|
"Kindle Road",
|
|
"Treasure Beach",
|
|
"Mt Ember",
|
|
"Cape Brink",
|
|
"Berry Forest",
|
|
"Bond Bridge",
|
|
"Three Isle Port",
|
|
"Four Island",
|
|
"Icefall Cave",
|
|
"Resort Gorgeous",
|
|
"Water Labyrinth",
|
|
"Five Island",
|
|
"Five Isle Meadow",
|
|
"Memorial Pillar",
|
|
"Outcast Island",
|
|
"Green Path",
|
|
"Water Path",
|
|
"Ruin Valley",
|
|
"Lost Cave",
|
|
"Pattern Bush",
|
|
"Trainer Tower",
|
|
"Canyon Entrance",
|
|
"Sevault Canyon",
|
|
"Tanoby Ruins",
|
|
"Monean Chamber",
|
|
"Liptoo Chamber",
|
|
"Weepth Chamber",
|
|
"Dilford Chamber",
|
|
"Scufib Chamber",
|
|
"Rixy Chamber",
|
|
"Viapos Chamber",
|
|
"Altering Cave",
|
|
],
|
|
"heartgold-soulsilver": [
|
|
# Johto
|
|
"Starter",
|
|
"New Bark Town",
|
|
"Route 29",
|
|
"Cherrygrove City",
|
|
"Route 30",
|
|
"Route 31",
|
|
"Dark Cave",
|
|
"Violet City",
|
|
"Sprout Tower",
|
|
"Route 32",
|
|
"Ruins Of Alph",
|
|
"Union Cave",
|
|
"Route 33",
|
|
"Slowpoke Well",
|
|
"Route 34",
|
|
"Ilex Forest",
|
|
"National Park",
|
|
"Route 35",
|
|
"Goldenrod City",
|
|
"Route 36",
|
|
"Route 37",
|
|
"Ecruteak City",
|
|
"Burned Tower",
|
|
"Bell Tower",
|
|
"Route 38",
|
|
"Route 39",
|
|
"Olivine City",
|
|
"Sea Route 40",
|
|
"Sea Route 41",
|
|
"Cianwood City",
|
|
"Route 42",
|
|
"Mt Mortar",
|
|
"Lake Of Rage",
|
|
"Route 43",
|
|
"Route 44",
|
|
"Ice Path",
|
|
"Blackthorn City",
|
|
"Dragons Den",
|
|
"Route 45",
|
|
"Route 46",
|
|
"Route 47",
|
|
"Route 48",
|
|
"Safari Zone",
|
|
"Whirl Islands",
|
|
"Tohjo Falls",
|
|
# Kanto post-game
|
|
"Route 27",
|
|
"Route 26",
|
|
"Victory Road 1",
|
|
"Route 28",
|
|
"Mt Silver",
|
|
"Pallet Town",
|
|
"Route 1",
|
|
"Viridian City",
|
|
"Viridian Forest",
|
|
"Pewter City",
|
|
"Route 2",
|
|
"Route 3",
|
|
"Mt Moon",
|
|
"Route 4",
|
|
"Cerulean City",
|
|
"Route 24",
|
|
"Route 25",
|
|
"Cerulean Cave",
|
|
"Route 5",
|
|
"Route 6",
|
|
"Vermilion City",
|
|
"Route 7",
|
|
"Route 8",
|
|
"Route 9",
|
|
"Route 10",
|
|
"Rock Tunnel",
|
|
"Route 11",
|
|
"Digletts Cave",
|
|
"Route 12",
|
|
"Route 13",
|
|
"Route 14",
|
|
"Route 15",
|
|
"Fuchsia City",
|
|
"Route 16",
|
|
"Route 17",
|
|
"Route 18",
|
|
"Celadon City",
|
|
"Sea Route 19",
|
|
"Sea Route 20",
|
|
"Seafoam Islands",
|
|
"Cinnabar Island",
|
|
"Sea Route 21",
|
|
"Route 22",
|
|
# Misc
|
|
"Unknown All Poliwag",
|
|
"Unknown All Rattata",
|
|
"Unknown All Bugs",
|
|
],
|
|
"emerald": [
|
|
"Starter",
|
|
"Route 101",
|
|
"Oldale Town",
|
|
"Route 103",
|
|
"Route 102",
|
|
"Petalburg City",
|
|
"Route 104",
|
|
"Petalburg Woods",
|
|
"Rusturf Tunnel",
|
|
"Route 116",
|
|
"Rustboro City",
|
|
"Route 105",
|
|
"Route 106",
|
|
"Dewford Town",
|
|
"Granite Cave",
|
|
"Route 107",
|
|
"Route 108",
|
|
"Route 109",
|
|
"Slateport City",
|
|
"Route 110",
|
|
"New Mauville",
|
|
"Route 117",
|
|
"Route 111",
|
|
"Mirage Tower",
|
|
"Route 112",
|
|
"Fiery Path",
|
|
"Jagged Pass",
|
|
"Lavaridge Town",
|
|
"Route 113",
|
|
"Route 114",
|
|
"Meteor Falls",
|
|
"Route 115",
|
|
"Route 118",
|
|
"Route 119",
|
|
"Route 120",
|
|
"Route 121",
|
|
"Safari Zone",
|
|
"Lilycove City",
|
|
"Route 122",
|
|
"Mt Pyre",
|
|
"Route 123",
|
|
"Magma Hideout",
|
|
"Route 124",
|
|
"Mossdeep City",
|
|
"Route 125",
|
|
"Shoal Cave",
|
|
"Route 126",
|
|
"Sootopolis City",
|
|
"Cave Of Origin",
|
|
"Route 127",
|
|
"Route 128",
|
|
"Seafloor Cavern",
|
|
"Route 129",
|
|
"Route 130",
|
|
"Route 131",
|
|
"Pacifidlog Town",
|
|
"Route 132",
|
|
"Route 133",
|
|
"Route 134",
|
|
"Ever Grande City",
|
|
"Victory Road",
|
|
"Sky Pillar",
|
|
"Abandoned Ship",
|
|
"Desert Underpass",
|
|
"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
|
|
ROUTE_ORDER["red-blue"] = ROUTE_ORDER["firered-leafgreen"]
|
|
ROUTE_ORDER["yellow"] = ROUTE_ORDER["firered-leafgreen"]
|
|
ROUTE_ORDER["lets-go"] = ROUTE_ORDER["firered-leafgreen"]
|
|
ROUTE_ORDER["gold-silver"] = ROUTE_ORDER["heartgold-soulsilver"]
|
|
ROUTE_ORDER["crystal"] = ROUTE_ORDER["heartgold-soulsilver"]
|
|
ROUTE_ORDER["ruby-sapphire"] = ROUTE_ORDER["emerald"]
|
|
ROUTE_ORDER["omega-ruby-alpha-sapphire"] = ROUTE_ORDER["emerald"]
|
|
|
|
# Collect all pokemon dex numbers across games
|
|
all_pokemon_dex: set[int] = set()
|
|
|
|
|
|
def clean_location_name(name: str) -> str:
|
|
"""Convert PokeAPI location slug to a clean display name.
|
|
|
|
e.g. 'kanto-route-1' -> 'Route 1'
|
|
'pallet-town' -> 'Pallet Town'
|
|
"""
|
|
for prefix in [
|
|
"kanto-", "johto-", "hoenn-", "sinnoh-",
|
|
"unova-", "kalos-", "alola-", "galar-",
|
|
]:
|
|
if name.startswith(prefix):
|
|
name = name[len(prefix):]
|
|
break
|
|
|
|
name = name.replace("-", " ").title()
|
|
name = re.sub(r"Route (\d+)", r"Route \1", name)
|
|
return name
|
|
|
|
|
|
def clean_area_name(area_name: str, location_name: str) -> str | None:
|
|
"""Extract meaningful area suffix, or None if it's the default area."""
|
|
if area_name.startswith(location_name):
|
|
suffix = area_name[len(location_name):].strip("-").strip()
|
|
if not suffix or suffix == "area":
|
|
return None
|
|
return suffix.replace("-", " ").title()
|
|
return area_name.replace("-", " ").title()
|
|
|
|
|
|
def sort_routes_by_progression(routes: list[dict], vg_key: str) -> list[dict]:
|
|
"""Sort routes by game progression order for the given version group.
|
|
|
|
Routes not in the ordering list fall to the end, sorted alphabetically.
|
|
Uses starts-with matching so 'Route 2' matches 'Route 2 (South Towards ...)'.
|
|
"""
|
|
order_list = ROUTE_ORDER.get(vg_key)
|
|
if not order_list:
|
|
return routes
|
|
|
|
def sort_key(route: dict) -> tuple[int, str]:
|
|
name = route["name"]
|
|
for i, ordered_name in enumerate(order_list):
|
|
if name == ordered_name or name.startswith(ordered_name + " ("):
|
|
return (i, name)
|
|
return (len(order_list), name) # Unmatched → end, alphabetical
|
|
|
|
return sorted(routes, key=sort_key)
|
|
|
|
|
|
def get_encounters_for_area(area_id: int, version_name: str) -> list[dict]:
|
|
"""Get encounter data for a location area, filtered by version."""
|
|
area = load_resource("location-area", area_id)
|
|
encounters = []
|
|
|
|
for pe in area["pokemon_encounters"]:
|
|
pokemon_url = pe["pokemon"]["url"]
|
|
dex_num = extract_id(pokemon_url)
|
|
pokemon_name = pe["pokemon"]["name"]
|
|
|
|
for vd in pe["version_details"]:
|
|
if vd["version"]["name"] != version_name:
|
|
continue
|
|
|
|
for enc in vd["encounter_details"]:
|
|
method = enc["method"]["name"]
|
|
if method not in INCLUDED_METHODS:
|
|
continue
|
|
|
|
encounters.append({
|
|
"pokemon_name": pokemon_name,
|
|
"national_dex": dex_num,
|
|
"method": method,
|
|
"chance": enc["chance"],
|
|
"min_level": enc["min_level"],
|
|
"max_level": enc["max_level"],
|
|
})
|
|
|
|
return encounters
|
|
|
|
|
|
def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]:
|
|
"""Aggregate encounter rates by pokemon + method (sum chances across level ranges)."""
|
|
agg: dict[tuple[int, str], dict] = {}
|
|
|
|
for enc in raw_encounters:
|
|
key = (enc["national_dex"], enc["method"])
|
|
if key not in agg:
|
|
agg[key] = {
|
|
"national_dex": enc["national_dex"],
|
|
"pokemon_name": enc["pokemon_name"],
|
|
"method": enc["method"],
|
|
"encounter_rate": 0,
|
|
"min_level": enc["min_level"],
|
|
"max_level": enc["max_level"],
|
|
}
|
|
agg[key]["encounter_rate"] += enc["chance"]
|
|
agg[key]["min_level"] = min(agg[key]["min_level"], enc["min_level"])
|
|
agg[key]["max_level"] = max(agg[key]["max_level"], enc["max_level"])
|
|
|
|
result = list(agg.values())
|
|
for r in result:
|
|
r["encounter_rate"] = min(r["encounter_rate"], 100)
|
|
|
|
return sorted(result, key=lambda x: (-x["encounter_rate"], x["pokemon_name"]))
|
|
|
|
|
|
def merge_special_encounters(routes: list[dict], special_data: dict[str, list[dict]]) -> list[dict]:
|
|
"""Merge special encounters into existing routes or create new ones."""
|
|
# Build lookup: route name -> route dict (including children)
|
|
route_map: dict[str, dict] = {}
|
|
for r in routes:
|
|
route_map[r["name"]] = r
|
|
for child in r.get("children", []):
|
|
route_map[child["name"]] = child
|
|
|
|
for location_name, encounters in special_data.items():
|
|
for enc in encounters:
|
|
all_pokemon_dex.add(enc["national_dex"])
|
|
|
|
if location_name in route_map:
|
|
route_map[location_name]["encounters"].extend(encounters)
|
|
else:
|
|
new_route = {"name": location_name, "order": 0, "encounters": encounters}
|
|
routes.append(new_route)
|
|
route_map[location_name] = new_route
|
|
|
|
return routes
|
|
|
|
|
|
def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]:
|
|
"""Process all locations for a specific game version.
|
|
|
|
Creates hierarchical route structure where locations with multiple areas
|
|
become parent routes with child routes for each area.
|
|
"""
|
|
print(f"\n--- Processing {version_name} ---")
|
|
|
|
region = load_resource("region", vg_info["region_id"])
|
|
location_refs = list(region["locations"])
|
|
|
|
# Include extra regions (e.g., Kanto for Johto games)
|
|
for extra_region_id in vg_info.get("extra_regions", []):
|
|
extra_region = load_resource("region", extra_region_id)
|
|
location_refs = location_refs + list(extra_region["locations"])
|
|
|
|
print(f" Found {len(location_refs)} locations")
|
|
|
|
routes = []
|
|
|
|
for loc_ref in location_refs:
|
|
loc_name = loc_ref["name"]
|
|
loc_id = extract_id(loc_ref["url"])
|
|
display_name = clean_location_name(loc_name)
|
|
|
|
location = load_resource("location", loc_id)
|
|
areas = location["areas"]
|
|
if not areas:
|
|
continue
|
|
|
|
all_encounters: list[dict] = []
|
|
area_specific: dict[str, list[dict]] = {}
|
|
|
|
for area_ref in areas:
|
|
area_id = extract_id(area_ref["url"])
|
|
area_slug = area_ref["name"]
|
|
area_suffix = clean_area_name(area_slug, loc_name)
|
|
|
|
encounters = get_encounters_for_area(area_id, version_name)
|
|
if not encounters:
|
|
continue
|
|
|
|
if area_suffix and len(areas) > 1:
|
|
area_specific[area_suffix] = encounters
|
|
else:
|
|
all_encounters.extend(encounters)
|
|
|
|
# If we have multiple area-specific encounters, create a parent route
|
|
# with child routes for each area (hierarchical grouping)
|
|
if area_specific and len(area_specific) > 1:
|
|
child_routes = []
|
|
for area_suffix, area_encs in area_specific.items():
|
|
aggregated = aggregate_encounters(area_encs)
|
|
if aggregated:
|
|
route_name = f"{display_name} ({area_suffix})"
|
|
for enc in aggregated:
|
|
all_pokemon_dex.add(enc["national_dex"])
|
|
child_routes.append({
|
|
"name": route_name,
|
|
"order": 0,
|
|
"encounters": aggregated,
|
|
})
|
|
|
|
# Only add parent if we have child routes
|
|
if child_routes:
|
|
routes.append({
|
|
"name": display_name,
|
|
"order": 0,
|
|
"encounters": [], # Parent routes have no encounters
|
|
"children": child_routes,
|
|
})
|
|
|
|
elif area_specific:
|
|
# Only one area-specific route - don't create parent/child, just use suffix
|
|
for area_suffix, area_encs in area_specific.items():
|
|
aggregated = aggregate_encounters(area_encs)
|
|
if aggregated:
|
|
route_name = f"{display_name} ({area_suffix})"
|
|
for enc in aggregated:
|
|
all_pokemon_dex.add(enc["national_dex"])
|
|
routes.append({
|
|
"name": route_name,
|
|
"order": 0,
|
|
"encounters": aggregated,
|
|
})
|
|
|
|
# Non-area-specific encounters (or single area without suffix)
|
|
if all_encounters:
|
|
aggregated = aggregate_encounters(all_encounters)
|
|
if aggregated:
|
|
for enc in aggregated:
|
|
all_pokemon_dex.add(enc["national_dex"])
|
|
routes.append({
|
|
"name": display_name,
|
|
"order": 0,
|
|
"encounters": aggregated,
|
|
})
|
|
|
|
# Merge special encounters (starters, gifts, fossils)
|
|
special_data = SPECIAL_ENCOUNTERS.get(vg_key, {})
|
|
if special_data:
|
|
routes = merge_special_encounters(routes, special_data)
|
|
|
|
# Sort routes by game progression order
|
|
routes = sort_routes_by_progression(routes, vg_key)
|
|
|
|
# Reassign sequential order values
|
|
order = 1
|
|
for route in routes:
|
|
route["order"] = order
|
|
order += 1
|
|
for child in route.get("children", []):
|
|
child["order"] = order
|
|
order += 1
|
|
|
|
# Count routes including children
|
|
total_routes = sum(1 + len(r.get("children", [])) for r in routes)
|
|
print(f" Routes with encounters: {total_routes}")
|
|
total_enc = sum(
|
|
len(r["encounters"]) + sum(len(c["encounters"]) for c in r.get("children", []))
|
|
for r in routes
|
|
)
|
|
print(f" Total encounter entries: {total_enc}")
|
|
|
|
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 + encountered forms from the local PokeAPI data."""
|
|
pokemon_dir = POKEAPI_DIR / "pokemon-species"
|
|
|
|
# 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:
|
|
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 + {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"]]
|
|
pokemon_list.append({
|
|
"national_dex": dex,
|
|
"name": poke["name"].title().replace("-", " "),
|
|
"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)} 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"])
|
|
|
|
|
|
def flatten_evolution_chain(chain: dict, seeded_dex: set[int]) -> list[dict]:
|
|
"""Recursively flatten a PokeAPI evolution chain into (from, to) pairs."""
|
|
pairs = []
|
|
from_dex = int(chain["species"]["url"].rstrip("/").split("/")[-1])
|
|
|
|
for evo in chain.get("evolves_to", []):
|
|
to_dex = int(evo["species"]["url"].rstrip("/").split("/")[-1])
|
|
|
|
for detail in evo["evolution_details"]:
|
|
trigger = detail["trigger"]["name"]
|
|
min_level = detail.get("min_level")
|
|
item = detail.get("item")
|
|
if item:
|
|
item = item["name"]
|
|
held_item = detail.get("held_item")
|
|
if held_item:
|
|
held_item = held_item["name"]
|
|
|
|
# Collect other conditions as a string
|
|
conditions = []
|
|
if detail.get("min_happiness"):
|
|
conditions.append(f"happiness >= {detail['min_happiness']}")
|
|
if detail.get("min_affection"):
|
|
conditions.append(f"affection >= {detail['min_affection']}")
|
|
if detail.get("min_beauty"):
|
|
conditions.append(f"beauty >= {detail['min_beauty']}")
|
|
if detail.get("time_of_day"):
|
|
conditions.append(detail["time_of_day"])
|
|
if detail.get("known_move"):
|
|
conditions.append(f"knows {detail['known_move']['name']}")
|
|
if detail.get("known_move_type"):
|
|
conditions.append(f"knows {detail['known_move_type']['name']}-type move")
|
|
if detail.get("location"):
|
|
conditions.append(f"at {detail['location']['name']}")
|
|
if detail.get("party_species"):
|
|
conditions.append(f"with {detail['party_species']['name']} in party")
|
|
if detail.get("party_type"):
|
|
conditions.append(f"with {detail['party_type']['name']}-type in party")
|
|
if detail.get("gender") is not None:
|
|
conditions.append("female" if detail["gender"] == 1 else "male")
|
|
if detail.get("needs_overworld_rain"):
|
|
conditions.append("raining")
|
|
if detail.get("turn_upside_down"):
|
|
conditions.append("turn upside down")
|
|
if detail.get("trade_species"):
|
|
conditions.append(f"trade for {detail['trade_species']['name']}")
|
|
if detail.get("relative_physical_stats") is not None:
|
|
stat_map = {1: "atk > def", -1: "atk < def", 0: "atk = def"}
|
|
conditions.append(stat_map.get(detail["relative_physical_stats"], ""))
|
|
|
|
condition = ", ".join(conditions) if conditions else None
|
|
|
|
if from_dex in seeded_dex and to_dex in seeded_dex:
|
|
pairs.append({
|
|
"from_national_dex": from_dex,
|
|
"to_national_dex": to_dex,
|
|
"trigger": trigger,
|
|
"min_level": min_level,
|
|
"item": item,
|
|
"held_item": held_item,
|
|
"condition": condition,
|
|
})
|
|
|
|
# Recurse into further evolutions
|
|
pairs.extend(flatten_evolution_chain(evo, seeded_dex))
|
|
|
|
return pairs
|
|
|
|
|
|
def fetch_evolution_data(seeded_dex: set[int]) -> list[dict]:
|
|
"""Fetch evolution chains from local PokeAPI data for all seeded pokemon."""
|
|
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(d for d in seeded_dex if d < 10000)
|
|
|
|
for i, dex in enumerate(dex_sorted, 1):
|
|
species = load_resource("pokemon-species", dex)
|
|
chain_url = species["evolution_chain"]["url"]
|
|
chain_id = extract_id(chain_url)
|
|
chain_ids.add(chain_id)
|
|
if i % 50 == 0 or i == len(dex_sorted):
|
|
print(f" Species fetched: {i}/{len(dex_sorted)}")
|
|
|
|
print(f" Found {len(chain_ids)} unique evolution chains")
|
|
|
|
# Fetch each chain and flatten
|
|
all_pairs: list[dict] = []
|
|
seen: set[tuple[int, int, str]] = set()
|
|
|
|
for chain_id in sorted(chain_ids):
|
|
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"])
|
|
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"]))
|
|
|
|
|
|
def apply_evolution_overrides(evolutions: list[dict]) -> None:
|
|
"""Apply overrides from evolution_overrides.json if it exists."""
|
|
overrides_path = DATA_DIR / "evolution_overrides.json"
|
|
if not overrides_path.exists():
|
|
return
|
|
|
|
with open(overrides_path) as f:
|
|
overrides = json.load(f)
|
|
|
|
# Remove entries
|
|
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"])
|
|
]
|
|
|
|
# Add entries
|
|
for addition in overrides.get("add", []):
|
|
evolutions.append({
|
|
"from_national_dex": addition["from_dex"],
|
|
"to_national_dex": addition["to_dex"],
|
|
"trigger": addition.get("trigger", "level-up"),
|
|
"min_level": addition.get("min_level"),
|
|
"item": addition.get("item"),
|
|
"held_item": addition.get("held_item"),
|
|
"condition": addition.get("condition"),
|
|
})
|
|
|
|
# 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"]):
|
|
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"]))
|
|
print(f" Applied overrides: {len(evolutions)} pairs after overrides")
|
|
|
|
|
|
def write_json(filename: str, data):
|
|
path = DATA_DIR / filename
|
|
with open(path, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
print(f" -> {path}")
|
|
|
|
|
|
def main():
|
|
if not POKEAPI_DIR.is_dir():
|
|
print(
|
|
f"Error: PokeAPI data not found at {POKEAPI_DIR}\n"
|
|
"Initialize the submodule with: git submodule update --init",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Build games.json
|
|
games = []
|
|
for vg_info in VERSION_GROUPS.values():
|
|
for game_info in vg_info["games"].values():
|
|
games.append({
|
|
"name": game_info["name"],
|
|
"slug": game_info["slug"],
|
|
"generation": vg_info["generation"],
|
|
"region": vg_info["region"],
|
|
"release_year": game_info["release_year"],
|
|
"color": game_info.get("color"),
|
|
})
|
|
|
|
write_json("games.json", games)
|
|
print(f"Wrote {len(games)} games to games.json")
|
|
|
|
# Process each version
|
|
for vg_key, vg_info in VERSION_GROUPS.items():
|
|
for ver_name in vg_info["versions"]:
|
|
routes = process_version(ver_name, vg_info, vg_key)
|
|
write_json(f"{ver_name}.json", routes)
|
|
|
|
# Fetch all Pokemon species
|
|
pokemon = fetch_all_pokemon()
|
|
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}
|
|
|
|
# Fetch evolution chains for all seeded Pokemon
|
|
evolutions = fetch_evolution_data(all_seeded_dex)
|
|
apply_evolution_overrides(evolutions)
|
|
write_json("evolutions.json", evolutions)
|
|
print(f"\nWrote {len(evolutions)} evolution pairs to evolutions.json")
|
|
|
|
print("\nDone! JSON files written to seeds/data/")
|
|
print("Review route ordering and curate as needed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|